Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/core/src/hooks/use-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@ export type StoreState = {
selectedNodeIds: string[]
isHelpOpen: boolean
isJsonInspectorOpen: boolean
sidebarWidth: number
wallsGroupRef: THREE.Group | null
activeTool: Tool | null
lastBuildingTool: Tool
Expand Down Expand Up @@ -388,6 +389,7 @@ export type StoreState = {

setIsHelpOpen: (open: boolean) => void
setIsJsonInspectorOpen: (open: boolean) => void
setSidebarWidth: (width: number) => void
closeSpecialEditors: () => Partial<StoreState>
setActiveTool: (tool: Tool | null, catalogCategory?: CatalogCategory | null) => void
setCatalogCategory: (category: CatalogCategory | null) => void
Expand Down Expand Up @@ -647,6 +649,7 @@ const useStore = create<StoreState>()(
selectedNodeIds: [],
isHelpOpen: false,
isJsonInspectorOpen: false,
sidebarWidth: 320,
wallsGroupRef: null,
activeTool: 'wall',
lastBuildingTool: 'wall',
Expand Down Expand Up @@ -808,6 +811,7 @@ const useStore = create<StoreState>()(

setIsHelpOpen: (open) => set({ isHelpOpen: open }),
setIsJsonInspectorOpen: (open) => set({ isJsonInspectorOpen: open }),
setSidebarWidth: (width) => set({ sidebarWidth: width }),

// Helper to close special editors (zones, site property lines) when changing modes/tools
// Returns partial state updates to be merged with other updates
Expand Down Expand Up @@ -1890,6 +1894,7 @@ const useStore = create<StoreState>()(
scene: sceneToStore,
selectedNodeIds: state.selectedNodeIds,
debug: state.debug,
sidebarWidth: state.sidebarWidth,
}
}

Expand All @@ -1901,6 +1906,7 @@ const useStore = create<StoreState>()(
// Don't persist selection state from transient scenes
selectedNodeIds: [],
debug: state.debug,
sidebarWidth: state.sidebarWidth,
}
}

Expand All @@ -1910,6 +1916,7 @@ const useStore = create<StoreState>()(
scene: state.scene,
selectedNodeIds: [],
debug: state.debug,
sidebarWidth: state.sidebarWidth,
}
},
onRehydrateStorage: () => (state) => {
Expand Down
32 changes: 30 additions & 2 deletions packages/editor/components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ import {
} from '@/components/ui/dialog'
import { Sidebar, SidebarContent, SidebarHeader } from '@/components/ui/sidebar'
import { useEditor } from '@/hooks/use-editor'
import { useResize } from './use-resize'
import { cn } from '@/lib/utils'

export function AppSidebar() {
const sidebarWidth = useEditor((state) => state.sidebarWidth)
const setSidebarWidth = useEditor((state) => state.setSidebarWidth)
const isHelpOpen = useEditor((state) => state.isHelpOpen)
const setIsHelpOpen = useEditor((state) => state.setIsHelpOpen)
const isJsonInspectorOpen = useEditor((state) => state.isJsonInspectorOpen)
Expand All @@ -39,6 +42,10 @@ export function AppSidebar() {
const [mounted, setMounted] = useState(false)
const [activePanel, setActivePanel] = useState<PanelId>('site')

const { isResizing, sidebarRef, startResizing } = useResize({
onWidthChange: setSidebarWidth,
})

// Wait for client-side hydration to complete before rendering store-dependent content
useEffect(() => {
setMounted(true)
Expand Down Expand Up @@ -176,8 +183,20 @@ export function AppSidebar() {
}

return (
<Sidebar className={cn('dark text-white')} variant="floating">
<div className="flex h-full">
<Sidebar
ref={sidebarRef}
className={cn('dark text-white')}
variant="floating"
style={{
width: `${sidebarWidth}px`,
// Disable animations only when resizing for better performance
...(isResizing && {
animationDuration: '0s',
transitionDuration: '0s',
}),
}}
>
<div className="flex h-full relative">
{/* Icon Rail */}
<IconRail activePanel={activePanel} onPanelChange={setActivePanel} />

Expand All @@ -192,6 +211,15 @@ export function AppSidebar() {
{renderPanelContent()}
</SidebarContent>
</div>

{/* Resize Handle */}
<div
className="absolute top-0 bottom-0 right-0 w-2 bg-border cursor-col-resize hover:bg-accent z-50 opacity-0 hover:opacity-100 transition-opacity"
onMouseDown={(e) => {
e.preventDefault()
startResizing()
}}
/>
</div>

{/* Dialogs */}
Expand Down
53 changes: 53 additions & 0 deletions packages/editor/components/use-resize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useEffect, useRef, useState } from 'react'

interface UseResizeOptions {
minWidth?: number
maxWidth?: number
onWidthChange?: (width: number) => void
}

interface UseResizeReturn {
isResizing: boolean
sidebarRef: React.RefObject<HTMLDivElement | null>
startResizing: () => void
}

export function useResize(options: UseResizeOptions = {}): UseResizeReturn {
const { minWidth = 200, maxWidth = 600, onWidthChange } = options

const [isResizing, setIsResizing] = useState(false)
const sidebarRef = useRef<HTMLDivElement>(null)

// Handle mouse move during resize
useEffect(() => {
if (!isResizing) return

const handleWidth = 8
const sidebarLeftGap = 10
const handleMouseMove = (e: MouseEvent) => {
const newWidth = Math.max(minWidth, Math.min(maxWidth, e.clientX + sidebarLeftGap + handleWidth / 2))
onWidthChange?.(newWidth)
}

const handleMouseUp = () => {
setIsResizing(false)
}

document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)

return () => {
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
}, [isResizing, minWidth, maxWidth, onWidthChange])

const startResizing = () => setIsResizing(true)


return {
isResizing,
sidebarRef,
startResizing,
}
}