+ )
+}
diff --git a/apps/editor/components/scene-loader.tsx b/apps/editor/components/scene-loader.tsx
index 1d8dd224..a3706740 100644
--- a/apps/editor/components/scene-loader.tsx
+++ b/apps/editor/components/scene-loader.tsx
@@ -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: ,
+ icon: (
+
+ ),
+ },
+ {
+ id: 'build',
+ label: 'Build',
+ component: BuildTab,
+ mobileDefaultSnap: 0.5,
+ mobileIcon: ,
+ icon: (
+
+ ),
},
]
diff --git a/apps/editor/public/icons/scene.png b/apps/editor/public/icons/scene.png
new file mode 100644
index 00000000..99036b8c
Binary files /dev/null and b/apps/editor/public/icons/scene.png differ
diff --git a/packages/editor/src/components/ui/sidebar/panels/items-panel/index.tsx b/packages/editor/src/components/ui/sidebar/panels/items-panel/index.tsx
index 73d58092..6f3c61d2 100644
--- a/packages/editor/src/components/ui/sidebar/panels/items-panel/index.tsx
+++ b/packages/editor/src/components/ui/sidebar/panels/items-panel/index.tsx
@@ -20,6 +20,8 @@ export function ItemsPanel({
leadingTile,
emptyState,
functionTree,
+ showSourceFilter = true,
+ showTagFilters = true,
}: {
items?: AssetInput[]
/** Called when the search query changes (community edition uses this for server-side search) */
@@ -41,6 +43,16 @@ export function ItemsPanel({
* hierarchical tree browse instead of the legacy hardcoded category tabs.
*/
functionTree?: FunctionTreeNode[]
+ /**
+ * Library/Community/Mine source chips. The open-source editor has no
+ * uploaded items (only the built-in catalog), so it hides these.
+ */
+ showSourceFilter?: boolean
+ /**
+ * Placement/functional tag filter chips under the search row. The
+ * open-source editor hides these to keep the panel to plain categories.
+ */
+ showTagFilters?: boolean
}) {
// When the embedder supplies a function taxonomy, the hierarchical browse
// replaces the legacy `furnishTools` category tabs entirely.
@@ -63,6 +75,8 @@ export function ItemsPanel({
leadingTile={leadingTile}
onSearchChange={onSearchChange}
searchResults={searchResults}
+ showSourceFilter={showSourceFilter}
+ showTagFilters={showTagFilters}
/>
}
@@ -72,12 +86,16 @@ function LegacyItemsPanel({
searchResults,
leadingTile,
emptyState,
+ showSourceFilter = true,
+ showTagFilters = true,
}: {
items?: AssetInput[]
onSearchChange?: (query: string) => void
searchResults?: AssetInput[] | null
leadingTile?: React.ReactNode
emptyState?: React.ReactNode
+ showSourceFilter?: boolean
+ showTagFilters?: boolean
}) {
const mode = useEditor((s) => s.mode)
const catalogCategory = useEditor((s) => s.catalogCategory)
@@ -89,8 +107,11 @@ function LegacyItemsPanel({
const [activeFunctionalTag, setActiveFunctionalTag] = useState(null)
// Library / Community / Mine. Default to Library so first-time users see
// the curated catalog rather than every uploaded item; clicking the chip
- // again clears the filter (`null` = show everything).
- const [activeSource, setActiveSource] = useState('library')
+ // again clears the filter (`null` = show everything). With the chips hidden
+ // there is nothing to filter by, so start unfiltered.
+ const [activeSource, setActiveSource] = useState(
+ showSourceFilter ? 'library' : null,
+ )
const [search, setSearch] = useState('')
const isServerSearch = onSearchChange !== undefined
// True when server search is active but results haven't come back yet
@@ -152,7 +173,7 @@ function LegacyItemsPanel({
const allTags = Array.from(new Set(categoryItems.flatMap((item) => item.tags ?? [])))
const placementTags = allTags.filter((t) => PLACEMENT_TAGS.has(t))
const functionalTags = allTags.filter((t) => !PLACEMENT_TAGS.has(t))
- const hasFilters = allTags.length > 1
+ const hasFilters = showTagFilters && allTags.length > 1
const placementCount = (tag: string | null) =>
categoryItems.filter((item) => {
@@ -205,9 +226,13 @@ function LegacyItemsPanel({
{/* Search and source filter take 50/50 of the row. `min-w-0` on
- both sides lets each half shrink to fit when the panel narrows. */}
+ both sides lets each half shrink to fit when the panel narrows.
+ With the source chips hidden, search spans the full row. */}
{
setSearch(e.target.value)
onSearchChange?.(e.target.value)
@@ -216,7 +241,7 @@ function LegacyItemsPanel({
type="text"
value={search}
/>
- {sourceChips.length > 0 && (
+ {showSourceFilter && sourceChips.length > 0 && (