From 48d5091bb37891cc499c2addb7dbc3a7fee9be2c Mon Sep 17 00:00:00 2001 From: Aymeric Rabot Date: Wed, 25 Feb 2026 18:05:02 -0500 Subject: [PATCH] UX polish: selection behavior, styling, and site panel improvements (#119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Remove .env.local guard; add portless dep Remove the check that blocked a local .env.local in apps/editor's dev script and simplify the dev startup to source the root .env. Add portless@^0.4.2 to the project dependencies in package.json. Lockfile updated accordingly. * Improve tool cursors, previews, and contextual tools Revamp editor tool visuals and behaviour: replace mesh cursors with a new CursorSphere Group (adds ground marker, vertical indicator and optional tool icon tooltip), unify preview colors to #818cf8, add ground-level previews and ghost lines for ceiling and roof tools, and update slab/zone/wall visuals to match. Add automatic build-mode switching and contextual highlighting for structure tools via a new useContextualTools hook, and relax action-menu visibility checks so tool rows are shown even when not in build mode. Minor typing/import adjustments and small opacity/placement tweaks across multiple tool components. * Keep tools active and refine action-menu styles Stop auto-deactivating tools in RoofTool and ZoneTool by removing setTool(null) (and setMode('select') in roof). Add hasActiveTool checks and small layout padding to FurnishTools and StructureTools, and update button classes to improve visual emphasis: smoother transitions, stronger active rings/scale, and dimming/grayscale of non-active tools when another tool is active. These changes improve UX by keeping the selected tool active and giving clearer visual feedback in the action menu. * Improve selection behavior and refresh UI styling SelectionManager: add global enter/leave/double-click handlers to support cross-phase hovering and auto-switching into structure/furnish on double-click; normalize click handling (explicitly coalesce shiftKey). Remove per-strategy enter/leave listeners. UI panels & controls: unify and modernize styles across many panels (ceiling, door, item, reference, roof, slab, wall, window) and action buttons — rounded-md, neutral borders, subtle shadows, translucent headers and consistent hover/focus states. Update view toggles icons and toggle styling. NumberInput: integrate @number-flow/react NumberFlow for formatted value display, add visual drag progress indicator and refine input/label interactions and styling. Sidebar: replace rename-popover with inline-rename-input and update tree nodes to use inline editing. Add slider-demo and tweak slider implementation. Update package.json accordingly. * Use Barlow font across editor UI Add the Google Barlow font and expose it as a CSS variable, update theme font variables in globals.css, and apply the new font across the editor UI. Imports: add Barlow in app/layout.tsx and include its variable on , set body to use font-sans. Apply font-barlow or font-mono classes to headings, labels, numeric readouts and buttons across multiple panels and primitives to standardize typography. Also update button base variant to include font-barlow. Remove the PhaseSwitcher component and its import/usage from the action menu. * Replace button wrapper with div in LevelItem Replace the outer - - -

- {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 ( @@ -178,16 +178,16 @@ export function ViewToggles() { 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²)`}

Direction -
+
{(['inward', 'outward'] as const).map((dir) => (
{/* Action Buttons */} -
+
- setBuildingCameraOpen(open ? building.id : null)} - > - - - - e.stopPropagation()} - > -
- {building.camera && ( - - )} - - {building.camera && ( - - )} -
-
-
-
- ))} -
- )} -
- ); -} - -// ============================================================================ -// 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 ( - - - - - - {buildings.map((building) => ( - + + e.stopPropagation()} > - - {building.name || "Building"} - - ))} - - +
+ {level.camera && ( + + )} + + {level.camera && ( + + )} +
+ + + + + + + + + {level.level !== 0 && ( + + )} + + +
); } @@ -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) => ( -
- - {/* Camera snapshot button */} - setCameraPopoverOpen(open ? level.id : null)}> - - - - e.stopPropagation()} - > -
- {level.camera && ( - - )} - - {level.camera && ( - - )} -
-
-
- - - - - - - {level.level !== 0 && ( - - )} - - -
+ 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)} - > - - -
- - - {zone.name || defaultName} - {/* Camera snapshot button */} - - - - - + + e.stopPropagation()} + > +
+ {PRESET_COLORS.map((color) => ( +
+
+
+ setIsEditing(false)} + onStartEditing={() => setIsEditing(true)} + defaultName={defaultName} + /> + {/* Camera snapshot button */} + + + - )} + + {zone.camera && ( + + )} + + + e.stopPropagation()} + > +
+ {zone.camera && ( - {zone.camera && ( - - )} -
-
-
- -
- + )} + + {zone.camera && ( + + )} +
+ + + +
); } @@ -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" && } -
- +
+
+ + setBuildingCameraOpen(open ? building.id : null)} + > + + + + e.stopPropagation()} + > +
+ {building.camera && ( + + )} + + {building.camera && ( + + )} +
+
+
+ + {/* 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")} + > +
+ 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 && ( + + )} +
+ ); + } + + 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()} - > -
- setValue(e.target.value)} - onKeyDown={handleKeyDown} - placeholder={defaultName} - className="flex-1 rounded border border-input bg-background px-2 py-1 text-sm outline-none focus:border-primary" - /> - - -
-
-
- ); -} diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx index 026020bf..371fe068 100644 --- a/apps/editor/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx +++ b/apps/editor/components/ui/sidebar/panels/site-panel/roof-tree-node.tsx @@ -2,7 +2,7 @@ import { RoofNode } 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"; @@ -12,7 +12,7 @@ interface RoofTreeNodeProps { } export function RoofTreeNode({ node, depth }: RoofTreeNodeProps) { - 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,7 +23,7 @@ export function RoofTreeNode({ node, depth }: RoofTreeNodeProps) { }; const handleDoubleClick = () => { - setRenameOpen(true); + setIsEditing(true); }; const handleMouseEnter = () => { @@ -40,28 +40,29 @@ export function RoofTreeNode({ node, depth }: RoofTreeNodeProps) { const defaultName = `Roof (${sizeLabel})`; return ( - - } - label={node.name || defaultName} - depth={depth} - hasChildren={false} - expanded={false} - onToggle={() => {}} - onClick={handleClick} - onDoubleClick={handleDoubleClick} - onMouseEnter={handleMouseEnter} - onMouseLeave={handleMouseLeave} - 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={handleClick} + onDoubleClick={handleDoubleClick} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + isSelected={isSelected} + isHovered={isHovered} + isVisible={node.visible !== false} + actions={} + /> ); } diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx index 9c222102..d720ac86 100644 --- a/apps/editor/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx +++ b/apps/editor/components/ui/sidebar/panels/site-panel/slab-tree-node.tsx @@ -2,7 +2,7 @@ import { SlabNode } 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"; @@ -12,7 +12,7 @@ interface SlabTreeNodeProps { } export function SlabTreeNode({ node, depth }: SlabTreeNodeProps) { - 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,7 +23,7 @@ export function SlabTreeNode({ node, depth }: SlabTreeNodeProps) { }; const handleDoubleClick = () => { - setRenameOpen(true); + setIsEditing(true); }; const handleMouseEnter = () => { @@ -39,29 +39,30 @@ export function SlabTreeNode({ node, depth }: SlabTreeNodeProps) { const defaultName = `Slab (${area}m²)`; return ( - - } - label={node.name || defaultName} - depth={depth} - hasChildren={false} - expanded={false} - onToggle={() => {}} - onClick={handleClick} - onDoubleClick={handleDoubleClick} - onMouseEnter={handleMouseEnter} - onMouseLeave={handleMouseLeave} - 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={handleClick} + onDoubleClick={handleDoubleClick} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} + isSelected={isSelected} + isHovered={isHovered} + isVisible={node.visible !== false} + actions={} + /> ); } diff --git a/apps/editor/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx b/apps/editor/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx index 52d3cc45..63160dcd 100644 --- a/apps/editor/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx +++ b/apps/editor/components/ui/sidebar/panels/site-panel/tree-node-actions.tsx @@ -42,7 +42,7 @@ export function TreeNodeActions({ node }: TreeNodeActionsProps) { return (