Two cross-cutting features that overlay the standard tab/section flow: a bottom dock for pinned sections, and a top search box that filters the toolbar to matching properties.
Two cross-cutting features that overlay the standard tab/section flow: a bottom dock for pinned sections, and a top search box that filters the toolbar to matching properties.
A bottom region of the toolbar that holds pinned section bodies. Pinning keeps a section's controls visible across selection changes — useful when you're tweaking, say, spacing or color across multiple nodes in a row.
Each PropertySection header carries a <SectionPinButton>. Clicking it adds
the section's id to pinnedIds in InspectorPinContext and the section body
gets portaled into a slot inside InspectorPinDock at the bottom.
| File | Role |
|---|---|
| InspectorPinContext.tsx | Context: pinnedIds, togglePin, registerSlot, getSlotNode |
| InspectorPinDock.tsx | Bottom region — collapsible, height-adjustable, hosts portal targets |
| SectionPinButton.tsx | The pin/unpin toggle in each section header |
The pinned section's body is not duplicated. PropertySection
(PropertySection.tsx) checks isPinned(sectionId) and getSlotNode(sectionId):
createPortal.This keeps a single React subtree for each section — same hooks, same state, same useNode() context, just a different DOM mount point.
InspectorPinDock.tsx:21-25 reads/writes via phStorage:
sidebar-inspector-pin-height — dock height in pixelssidebar-inspector-pin-dock — open/closed booleanpinnedIds in InspectorPinContext's storage hook.)The dock height also gets reflected to --sidebar-inspector-pin-height CSS
var on #toolbar so the scroll body can reserve space and avoid overlap:
toolbar.style.setProperty("--sidebar-inspector-pin-height", `${h}px`);
null, takes no space.Drag handle uses pointer-move math — no library — clamped to
[MIN_HEIGHT, 60% of toolbar height].
ref={el => ...} retriggers
ref churn → infinite setState. The dock memoizes ref callbacks per section
id (InspectorPinDock.tsx:39-54).<div ref={refForSlot(id)} /> first; the section's body portals into
that node on its next render.Typing in the search box (SettingsSearchAtom) replaces the stacked tabs with a single results list of matched properties — no tab navigation during search.
<InspectorBody
sections={
isSearching
? [{ title: `Results for "${search}"`, children: <SearchResults search={search} /> }]
: STATIC_SECTIONS
}
/>
A single fake "Results" section replaces all 5 stacks while searching. Tab strip becomes a single-tab strip.
searchProperties(query) (propertyRegistry.ts) returns
Map<sectionId, PropertyDef[]> — properties grouped by their home section so
results stay organized. Matches against def.label + def.keywords.
searchOnly properties are included only here (never in the normal stack
view).
RegistrySettings.tsx:233-271 (<SearchEffects>) does two things:
Auto-expand accordions: accordionCtx.openAll() so collapsed sections
inside results show their bodies.
Visual highlight: uses the CSS Custom Highlight API to
highlight matched substrings inside #toolbarContents. No DOM mutation —
CSS.highlights.set("settings-search", new Highlight(...ranges)).
Uses a TreeWalker to find every text node, builds Ranges for each
match, registers them with the global highlights map. Cleanup runs the
delete on every search change.
if (typeof CSS === "undefined" || !("highlights" in CSS)) return;
guard short-circuits in older browsers. Search still works without it —
just no visual highlight.searchOnly is intentional. Some properties are too niche for the main
view (e.g. weird CSS overrides). Marking them searchOnly: true keeps them
discoverable without cluttering tabs.