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
@@ -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<string | null>(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<AssetInput['source'] | null>('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<AssetInput['source'] | null>(
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({
<div className="flex shrink-0 flex-col gap-2 border-border/70 border-b p-2">
<div className="flex items-center gap-1.5">
{/* 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. */}
<input
className="w-1/2 min-w-0 shrink-0 rounded-lg bg-muted px-2.5 py-1.5 text-xs placeholder:text-muted-foreground focus:outline-none"
className={cn(
'min-w-0 shrink-0 rounded-lg bg-muted px-2.5 py-1.5 text-xs placeholder:text-muted-foreground focus:outline-none',
showSourceFilter ? 'w-1/2' : 'w-full',
)}
onChange={(e) => {
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 && (
<div className="flex w-1/2 min-w-0 shrink-0 rounded-lg bg-muted p-0.5">
{sourceChips.map((chip) => {
const isActive = activeSource === chip.id