diff --git a/apps/editor/src/ui/editor/treePanel/FrameworkTreeList.tsx b/apps/editor/src/ui/editor/treePanel/FrameworkTreeList.tsx
index cc4b2a5..158e385 100644
--- a/apps/editor/src/ui/editor/treePanel/FrameworkTreeList.tsx
+++ b/apps/editor/src/ui/editor/treePanel/FrameworkTreeList.tsx
@@ -19,6 +19,8 @@ type Props = {
noHeader?: boolean
/** Optional extra content rendered inside the header (e.g. a framework selector dropdown) */
headerSlot?: React.ReactNode
+ /** Optional content rendered below the header, above the scrollable tree (e.g. an "Add item" action) */
+ belowHeaderSlot?: React.ReactNode
onSelect?: (_id: string) => void
onAddChild?: (_parentId: string) => void
onAddRoot?: () => void
@@ -50,6 +52,7 @@ export default function FrameworkTreeList({
onToggleExpand,
noHeader,
headerSlot,
+ belowHeaderSlot,
onSelect,
onAddChild,
onAddRoot,
@@ -69,6 +72,7 @@ export default function FrameworkTreeList({
{!noHeader &&
}
+ {belowHeaderSlot ? (
+
+ {belowHeaderSlot}
+
+ ) : null}
+
{roots.length === 0 ? (
diff --git a/apps/editor/src/ui/editor/treePanel/TreePanelView.tsx b/apps/editor/src/ui/editor/treePanel/TreePanelView.tsx
index 38a3486..bdb8942 100644
--- a/apps/editor/src/ui/editor/treePanel/TreePanelView.tsx
+++ b/apps/editor/src/ui/editor/treePanel/TreePanelView.tsx
@@ -237,6 +237,11 @@ export default function TreePanelView({ availableFrameworks = [], serverFramewor
const leftPanelRef = useRef(null)
const rightPanelRef = useRef(null)
+ // Right panel's static header has no variable content, so it's kept in sync with the
+ // left panel's header height (which varies with title/description/publisher) instead
+ // of hard-coding a height — that way the "Add" buttons below each header stay aligned.
+ const [leftHeaderHeight, setLeftHeaderHeight] = useState(undefined)
+
// Stable refs for values used inside recalculateLines (avoids stale closures)
const pendingRef = useRef(pendingAssociations)
pendingRef.current = pendingAssociations
@@ -444,6 +449,19 @@ export default function TreePanelView({ availableFrameworks = [], serverFramewor
}
}, [recalculateLines, expandedTargetId])
+ // Track the left panel's header height so the right panel's header can match it
+ useEffect(() => {
+ const header = leftPanelRef.current?.querySelector('[data-tree-panel-header]')
+ if (!header) return
+
+ const sync = () => setLeftHeaderHeight(header.getBoundingClientRect().height)
+ sync()
+
+ const obs = new ResizeObserver(sync)
+ obs.observe(header)
+ return () => obs.disconnect()
+ }, [frameworkInfo.title, cfDocument?.description, cfDocument?.publisher])
+
// Close popover on outside click
useEffect(() => {
if (!popover) return
@@ -876,6 +894,16 @@ export default function TreePanelView({ availableFrameworks = [], serverFramewor
onDragStart={NOOP_DRAG_START}
associationCounts={leftAssociationCounts}
onBadgeClick={handleLeftBadgeClick}
+ belowHeaderSlot={(
+
+ )}
/>
{/* Gate callout — shown only when a target is expanded but source hasn't been saved */}
@@ -887,26 +915,30 @@ export default function TreePanelView({ availableFrameworks = [], serverFramewor
)}
-
-
-
{/* Right panel — accordion list of target frameworks */}