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
23 changes: 21 additions & 2 deletions apps/editor/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
'use client'

import { Editor } from '@pascal-app/editor'
import {
Editor,
type SidebarTab,
ViewerToolbarLeft,
ViewerToolbarRight,
} from '@pascal-app/editor'

const SIDEBAR_TABS: (SidebarTab & { component: React.ComponentType })[] = [
{
id: 'site',
label: 'Scene',
component: () => null, // Built-in SitePanel handles this
},
]

export default function Home() {
return (
<div className="h-screen w-screen">
<Editor projectId="local-editor" />
<Editor
layoutVersion="v2"
projectId="local-editor"
sidebarTabs={SIDEBAR_TABS}
viewerToolbarLeft={<ViewerToolbarLeft />}
viewerToolbarRight={<ViewerToolbarRight />}
/>
</div>
)
}
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

209 changes: 209 additions & 0 deletions packages/editor/src/components/editor/editor-layout-v2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
'use client'

import { type ReactNode, useCallback, useEffect, useRef } from 'react'
import useEditor from '../../store/use-editor'
import { useSidebarStore } from '../ui/primitives/sidebar'
import { type SidebarTab, TabBar } from '../ui/sidebar/tab-bar'

const SIDEBAR_MIN_WIDTH = 300
const SIDEBAR_MAX_WIDTH = 800
const SIDEBAR_COLLAPSE_THRESHOLD = 220

// ── Left column: resizable panel with tab bar ────────────────────────────────

function LeftColumn({
tabs,
renderTabContent,
}: {
tabs: SidebarTab[]
renderTabContent: (tabId: string) => ReactNode
}) {
const width = useSidebarStore((s) => s.width)
const isCollapsed = useSidebarStore((s) => s.isCollapsed)
const setIsCollapsed = useSidebarStore((s) => s.setIsCollapsed)
const setWidth = useSidebarStore((s) => s.setWidth)
const isDragging = useSidebarStore((s) => s.isDragging)
const setIsDragging = useSidebarStore((s) => s.setIsDragging)
const activePanel = useEditor((s) => s.activeSidebarPanel)
const setActivePanel = useEditor((s) => s.setActiveSidebarPanel)

const isResizing = useRef(false)
const isExpanding = useRef(false)

// Ensure active panel is a valid tab
useEffect(() => {
if (tabs.length > 0 && !tabs.some((t) => t.id === activePanel)) {
setActivePanel(tabs[0]!.id)
}
}, [tabs, activePanel, setActivePanel])

const handleResizerDown = useCallback(
(e: React.PointerEvent) => {
e.preventDefault()
isResizing.current = true
setIsDragging(true)
document.body.style.cursor = 'col-resize'
document.body.style.userSelect = 'none'
},
[setIsDragging],
)

const handleGrabDown = useCallback(
(e: React.PointerEvent) => {
e.preventDefault()
isExpanding.current = true
setIsDragging(true)
document.body.style.cursor = 'col-resize'
document.body.style.userSelect = 'none'
},
[setIsDragging],
)

useEffect(() => {
const handlePointerMove = (e: PointerEvent) => {
if (isResizing.current) {
const newWidth = e.clientX
if (newWidth < SIDEBAR_COLLAPSE_THRESHOLD) {
setIsCollapsed(true)
} else {
setIsCollapsed(false)
setWidth(Math.max(SIDEBAR_MIN_WIDTH, Math.min(newWidth, SIDEBAR_MAX_WIDTH)))
}
} else if (isExpanding.current && e.clientX > 60) {
setIsCollapsed(false)
setWidth(Math.max(SIDEBAR_MIN_WIDTH, Math.min(e.clientX, SIDEBAR_MAX_WIDTH)))
}
}
const handlePointerUp = () => {
isResizing.current = false
isExpanding.current = false
setIsDragging(false)
document.body.style.cursor = ''
document.body.style.userSelect = ''
}
window.addEventListener('pointermove', handlePointerMove)
window.addEventListener('pointerup', handlePointerUp)
return () => {
window.removeEventListener('pointermove', handlePointerMove)
window.removeEventListener('pointerup', handlePointerUp)
}
}, [setWidth, setIsCollapsed, setIsDragging])

if (isCollapsed) {
return (
<div
className="relative h-full w-2 flex-shrink-0 cursor-col-resize transition-colors hover:bg-primary/20"
onPointerDown={handleGrabDown}
title="Expand sidebar"
/>
)
}

return (
<div
className="relative z-10 flex h-full flex-shrink-0 flex-col bg-sidebar text-sidebar-foreground"
style={{
width,
transition: isDragging ? 'none' : 'width 150ms ease',
}}
>
<TabBar activeTab={activePanel} onTabChange={setActivePanel} tabs={tabs} />
<div className="flex flex-1 flex-col overflow-hidden">{renderTabContent(activePanel)}</div>

{/* Resize handle + hit area */}
<div
className="absolute inset-y-0 -right-3 z-[100] flex w-6 cursor-col-resize items-center justify-center"
onPointerDown={handleResizerDown}
>
<div className="h-8 w-1 rounded-full bg-neutral-500" />
</div>
</div>
)
}

// ── Right column: viewer area with toolbar ───────────────────────────────────

function RightColumn({
toolbarLeft,
toolbarRight,
children,
overlays,
}: {
toolbarLeft?: ReactNode
toolbarRight?: ReactNode
children: ReactNode
overlays?: ReactNode
}) {
return (
<div
className="relative flex min-w-0 flex-1 flex-col overflow-hidden"
style={{
borderTopLeftRadius: 16,
clipPath: 'inset(0 0 0 0 round 16px 0 0 0)',
boxShadow: '-4px -2px 16px rgba(0, 0, 0, 0.08), -1px 0 4px rgba(0, 0, 0, 0.04)',
}}
>
{/* Viewer toolbar */}
{(toolbarLeft || toolbarRight) && (
<div className="pointer-events-none absolute top-3 right-3 left-3 z-20 flex items-center justify-between gap-2">
<div className="pointer-events-auto flex items-center gap-2">{toolbarLeft}</div>
<div className="pointer-events-auto flex items-center gap-2">{toolbarRight}</div>
</div>
)}
{/* Canvas area */}
<div className="relative flex-1 overflow-hidden">{children}</div>
{/* Overlays scoped to the viewer column */}
{overlays && (
<div
className="pointer-events-none absolute inset-0 z-30"
style={{ transform: 'translateZ(0)' }}
>
{overlays}
</div>
)}
</div>
)
}

// ── Main v2 layout ───────────────────────────────────────────────────────────

export interface EditorLayoutV2Props {
navbarSlot?: ReactNode
sidebarTabs?: SidebarTab[]
renderTabContent: (tabId: string) => ReactNode
viewerToolbarLeft?: ReactNode
viewerToolbarRight?: ReactNode
viewerContent: ReactNode
overlays?: ReactNode
}

export function EditorLayoutV2({
navbarSlot,
sidebarTabs = [],
renderTabContent,
viewerToolbarLeft,
viewerToolbarRight,
viewerContent,
overlays,
}: EditorLayoutV2Props) {
return (
<div className="dark flex h-full w-full flex-col bg-sidebar text-foreground">
{/* Top navbar */}
{navbarSlot}

{/* Main content: left column + right column */}
<div className="flex min-h-0 flex-1">
{sidebarTabs.length > 0 && (
<LeftColumn renderTabContent={renderTabContent} tabs={sidebarTabs} />
)}
<RightColumn
overlays={overlays}
toolbarLeft={viewerToolbarLeft}
toolbarRight={viewerToolbarRight}
>
{viewerContent}
</RightColumn>
</div>
</div>
)
}
Loading