feat(editor): build sidebar + slimmed items panel for standalone editor (#361)

Bring the open-source standalone editor closer to the community editor's
v2 sidebar without pulling in any preset/catalog infrastructure.

- Add a preset-less Build tab (apps/editor) that mirrors the community
  Build sidebar: wall, fence, slab, ceiling, roof, stair, elevator,
  door, window, column, spawn, plus the material-paint panel. Clicking a
  type activates the raw structure tool drawn with the kind's defaults.
- Wire the Build tab into both editor mount points (local + saved scene)
  and give the left rail proper image icons (Scene/Build/Items/Settings)
  instead of letter fallbacks.
- Gate the ItemsPanel Library/Community/Mine source chips and tag filter
  rows behind `showSourceFilter` / `showTagFilters` props (default true,
  so community and external consumers are unchanged). The standalone
  editor passes both off, leaving plain category tabs + full-width search.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Wassim SAMAD
2026-06-02 14:51:09 -04:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 7f49efce68
commit e450d8b474
5 changed files with 275 additions and 8 deletions
+54 -2
View File
@@ -1,13 +1,22 @@
'use client'
import { Editor, ItemsPanel } from '@pascal-app/editor'
import { Layers, Package, Settings } from 'lucide-react'
import { Hammer, Layers, Package, Settings } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
import { BuildTab } from '@/components/build-tab'
import {
CommunityViewerToolbarLeft,
CommunityViewerToolbarRight,
} from '@/components/viewer-toolbar'
// The open-source editor only ships the built-in catalog (no uploaded items),
// so the Library/Community/Mine source chips and tag filters add nothing —
// drop them and keep the panel to plain categories.
function EditorItemsPanel() {
return <ItemsPanel showSourceFilter={false} showTagFilters={false} />
}
const SIDEBAR_TABS = [
{
id: 'site',
@@ -15,13 +24,47 @@ const SIDEBAR_TABS = [
component: () => null,
mobileDefaultSnap: 0.5,
mobileIcon: <Layers className="h-5 w-5" />,
icon: (
<Image
alt=""
className="h-8 w-8 object-contain"
height={32}
src="/icons/scene.png"
width={32}
/>
),
},
{
id: 'build',
label: 'Build',
component: BuildTab,
mobileDefaultSnap: 0.5,
mobileIcon: <Hammer className="h-5 w-5" />,
icon: (
<Image
alt=""
className="h-8 w-8 object-contain"
height={32}
src="/icons/build.png"
width={32}
/>
),
},
{
id: 'items',
label: 'Items',
component: ItemsPanel,
component: EditorItemsPanel,
mobileDefaultSnap: 0.5,
mobileIcon: <Package className="h-5 w-5" />,
icon: (
<Image
alt=""
className="h-8 w-8 object-contain"
height={32}
src="/icons/couch.png"
width={32}
/>
),
},
{
id: 'settings',
@@ -29,6 +72,15 @@ const SIDEBAR_TABS = [
component: () => null,
mobileDefaultSnap: 0.5,
mobileIcon: <Settings className="h-5 w-5" />,
icon: (
<Image
alt=""
className="h-8 w-8 object-contain"
height={32}
src="/icons/settings.png"
width={32}
/>
),
},
]
+160
View File
@@ -0,0 +1,160 @@
'use client'
import { MaterialPaintPanel, useEditor } from '@pascal-app/editor'
import Image from 'next/image'
import { useCallback, useEffect, useRef } from 'react'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/toolbar-tooltip'
import { cn } from '@/lib/utils'
/**
* Raw structure-tool kinds the Build tab can activate. These map 1:1 to the
* editor's `StructureTool` ids.
*/
type BuildToolKind =
| 'wall'
| 'fence'
| 'slab'
| 'ceiling'
| 'roof'
| 'stair'
| 'elevator'
| 'door'
| 'window'
| 'column'
| 'spawn'
type BuildType = {
/** Selection id — equals `kind` for tool types, `'painting'` for paint mode. */
id: string
label: string
iconSrc: string
/** Present for structure-tool types (absent for the paint mode). */
kind?: BuildToolKind
/** Non-placement special mode. */
mode?: 'material-paint'
}
// Same icons + ordering as the community Build sidebar, minus presets.
const BUILD_TYPES: BuildType[] = [
{ id: 'wall', label: 'Wall', iconSrc: '/icons/wall.png', kind: 'wall' },
{ id: 'fence', label: 'Fence', iconSrc: '/icons/fence.png', kind: 'fence' },
{ id: 'slab', label: 'Slab', iconSrc: '/icons/floor.png', kind: 'slab' },
{ id: 'ceiling', label: 'Ceiling', iconSrc: '/icons/ceiling.png', kind: 'ceiling' },
{ id: 'roof', label: 'Roof', iconSrc: '/icons/roof.png', kind: 'roof' },
{ id: 'stair', label: 'Stairs', iconSrc: '/icons/stairs.png', kind: 'stair' },
{ id: 'elevator', label: 'Elevator', iconSrc: '/icons/elevator.png', kind: 'elevator' },
{ id: 'door', label: 'Door', iconSrc: '/icons/door.png', kind: 'door' },
{ id: 'window', label: 'Window', iconSrc: '/icons/window.png', kind: 'window' },
{ id: 'column', label: 'Column', iconSrc: '/icons/column.png', kind: 'column' },
{ id: 'spawn', label: 'Spawn Point', iconSrc: '/icons/site.png', kind: 'spawn' },
{ id: 'painting', label: 'Painting', iconSrc: '/icons/paint.png', mode: 'material-paint' },
]
/**
* Activate a raw structure draw/cursor tool. Mirrors the editor's own
* structure-tool activation (`setPhase`/`setStructureLayer`/`setMode`/`setTool`).
*/
function activateBuildTool(kind: BuildToolKind): void {
const ed = useEditor.getState()
ed.setPhase('structure')
ed.setStructureLayer('elements')
ed.setCatalogCategory(null)
ed.setToolDefaults(kind, null)
ed.setMode('build')
ed.setTool(kind)
}
/** Enter material-paint mode — the Build tab's "Painting" category. */
function activatePaintMode(): void {
const ed = useEditor.getState()
ed.setPhase('structure')
ed.setStructureLayer('elements')
ed.setMode('material-paint')
}
/**
* Build tab for the open-source standalone editor — a preset-less replica of
* the community Build sidebar. Clicking a type activates its raw tool, drawn
* with the kind's own `def.defaults()`. The "Painting" type swaps in the
* material-paint panel.
*/
export function BuildTab() {
const activeTool = useEditor((s) => s.tool)
const mode = useEditor((s) => s.mode)
const isTypeActive = (type: BuildType) =>
type.mode === 'material-paint'
? mode === 'material-paint'
: mode === 'build' && activeTool === type.kind
const handleTypeClick = useCallback((type: BuildType) => {
if (type.mode === 'material-paint') {
activatePaintMode()
} else if (type.kind) {
activateBuildTool(type.kind)
}
}, [])
// On open, land on the first build tool — parity with the community Build
// sidebar, so switching to Build immediately arms a usable tool.
const didInitRef = useRef(false)
useEffect(() => {
if (didInitRef.current) return
didInitRef.current = true
const firstType = BUILD_TYPES.find((t) => t.kind)
if (firstType) handleTypeClick(firstType)
}, [handleTypeClick])
return (
<div className="flex h-full flex-col gap-3 p-3">
<TooltipProvider delayDuration={0} disableHoverableContent>
<div
className="grid gap-1.5"
style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(56px, 1fr))' }}
>
{BUILD_TYPES.map((type) => {
const active = isTypeActive(type)
return (
<Tooltip key={type.id}>
<TooltipTrigger asChild>
<button
className={cn(
'group relative flex aspect-square items-center justify-center rounded-xl p-1 transition-all duration-200',
active
? 'bg-primary/10 ring-1 ring-primary/50'
: 'bg-muted/40 opacity-70 grayscale hover:bg-muted hover:opacity-100 hover:grayscale-0',
)}
onClick={() => handleTypeClick(type)}
type="button"
>
<Image
alt={type.label}
className="size-full object-contain transition-transform duration-200 group-hover:scale-110"
height={48}
src={type.iconSrc}
width={48}
/>
</button>
</TooltipTrigger>
<TooltipContent className="pointer-events-none" side="top">
{type.label}
</TooltipContent>
</Tooltip>
)
})}
</div>
</TooltipProvider>
{mode === 'material-paint' ? (
<div className="min-h-0 flex-1 overflow-y-auto">
<MaterialPaintPanel />
</div>
) : null}
</div>
)
}
+30
View File
@@ -9,9 +9,12 @@ import {
type SceneGraph,
type SidebarTab,
} from '@pascal-app/editor'
import { Hammer, Layers } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useCallback, useEffect, useRef, useState } from 'react'
import { BuildTab } from './build-tab'
import { CommunityViewerToolbarLeft, CommunityViewerToolbarRight } from './viewer-toolbar'
export interface SceneMeta {
@@ -32,6 +35,33 @@ const SIDEBAR_TABS: (SidebarTab & { component: React.ComponentType })[] = [
id: 'site',
label: 'Scene',
component: () => null, // Built-in SitePanel handles this
mobileDefaultSnap: 0.5,
mobileIcon: <Layers className="h-5 w-5" />,
icon: (
<Image
alt=""
className="h-8 w-8 object-contain"
height={32}
src="/icons/scene.png"
width={32}
/>
),
},
{
id: 'build',
label: 'Build',
component: BuildTab,
mobileDefaultSnap: 0.5,
mobileIcon: <Hammer className="h-5 w-5" />,
icon: (
<Image
alt=""
className="h-8 w-8 object-contain"
height={32}
src="/icons/build.png"
width={32}
/>
),
},
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB