The 5-tab strip pinned at the top of the right toolbar, plus the IntersectionObserver-based logic that keeps the active pill in sync with the scroll position of the body.
The 5-tab strip pinned at the top of the right toolbar, plus the IntersectionObserver-based logic that keeps the active pill in sync with the scroll position of the body.
const FIXED_HEAD = [
{ title: "Component", icon: null }, // tab[0] — swaps per node
{ title: "Layout", icon: <TbBoxPadding /> },
{ title: "Design", icon: <BiPaint /> },
{ title: "Interactions", icon: <TbMouse /> },
{ title: "Advanced", icon: <TbSettings /> },
];
const TAB_IDS = ["component", "layout", "design", "interactions", "advanced"];
The strip is static — tabs and stacks render once, never unmount. Switching
the selected node does not rebuild the tree; only content inside reacts via
useNode() / useEditor() hooks. (See the file header on
RegistrySettings.tsx.)
headRef.current[0].title = displayName || "Component";
headRef.current[0].icon = <NodeIcon />;
// ...
STATIC_SECTIONS[0].title = displayName || "Component";
displayName comes from node.data.displayName || node.data.name.toolbar.icon on the component's craft def, resolved by
resolveToolboxIcon.⚠️ Don't "fix" the ref mutation by lifting it to state. Doing so will remount the entire strip on every selection change.
The body of tab[0] is rendered by <ComponentSection>, which pulls
toolbar.settings from the craft def and renders that component as-is. See
main-tabs.md for the per-component MainTab system that fills it.
A click on a tab calls setActiveTabFromClick(title, setTab) (InspectorTab.tsx:145-148)
which does two things:
TabAtom to the clicked title.clickLockUntil = Date.now() + 400 — a 400ms window during which
scroll-driven updates are suppressed so the indicator doesn't fight the
click.Then scrollToSection(title, stackIndex) (InspectorTab.tsx:218-233) does the actual jump
via section.scrollIntoView({ behavior: "instant", block: "start" }) and a
-7px correction so the section header isn't flush with the container top.
Stack indices: Component=0, Layout=1, Design=2, Interactions=3, Advanced=4.
InspectorTab.tsx:80-114 creates one observer per <InspectorSection>:
#toolbarContents (the scroll container).[0, 0.1, 0.25, 0.5] — the observer fires at multiple scroll
positions so the active state updates smoothly.When a stack becomes active, onVisibilityChange flips the entry in
visibleSections, and <InspectorBody> finds the lowest-index visible stack
and writes its title to TabAtom.
The Date.now() > clickLockUntil check (InspectorTab.tsx:163) is what keeps a
click-driven tab change from being clobbered by an immediate scroll fire.
If you ever wrap the body in a different scroll container, copy the id
#toolbarContentsover — the observer silently no-ops without it.
{
/* Spacer so the last section can scroll fully to the top */
}
<div className="shrink-0" style={{ minHeight: "70vh" }} />;
Without this, the "Advanced" stack can never scroll its top edge to the container top → the Advanced tab pill could never reach the active state via scroll.
useScrollToActiveTab(activeTab, setActiveTab, id) (RegistrySettings.tsx:197)
runs whenever the selected node changes. It scrolls back to the previously
active tab's stack so users don't lose context when clicking between similar
nodes.
#toolbarContents is load-bearing. It's the IntersectionObserver root
and the scroll target for scrollToSection. Don't rename it without
searching for all usages.setActiveTabFromClick so scroll tracking doesn't
immediately overwrite it.STATIC_SECTIONS[0].title
mutation on every render.