Merge upstream/main into pr/panel-resize

Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
Said TurkseverandClaude Fable 5.1 committed 2026-09-23 19:00:18 +01:00
commit 11603729dc
234 files changed
+45360 -944

No files matched your search

+64 -43
View File
@@ -7,6 +7,8 @@ import {
shouldHideCollapsedRightPanels,
} from './panelRailGeometry.js';
const pendingCollapseRetries = new WeakSet();
/**
* Measure and place the right panel rail for one synchronous layout pass.
* The caller owns scheduling, obstacle selection, disclosure preferences and
@@ -40,11 +42,14 @@ export function layoutRightPanelRail({
getComputedStyle = (element) => windowRef.getComputedStyle(element),
}) {
if (!stack) return;
// A collapse may schedule one follow-up, which only measures and allocates.
const isCollapseRetry = pendingCollapseRetries.delete(stack);
// A panel lifted out of the rail (PanelPositionControls' portable mode)
// is a fixed window the rail neither measures nor allocates.
const panels = [...stack.children].filter((panel) =>
panel.matches('[data-panel-id]:not(.panel-floating)'),
const panels = [...stack.children].filter(
(panel) =>
panel.matches('[data-panel-id]:not(.panel-floating)') && !panel.hidden,
);
if (!hud.visible || hud.variant !== 'tactical') {
for (const panel of panels.filter((item) =>
@@ -115,33 +120,53 @@ export function layoutRightPanelRail({
});
}
// Auto-collapsed headers contribute the same need even when exclusive
// presentation hides them. Manually collapsed launchers keep their policy.
const measuredPanels = panels.filter(
(panel) =>
!exclusive ||
!panel.classList.contains('collapsed') ||
panel.classList.contains('layout-auto-collapsed'),
);
const visiblePanels = panels.filter(
(panel) => !exclusive || !panel.classList.contains('collapsed'),
);
const displayScrollTop = readDisplayScrollTop();
// Measure intrinsic content, not the allocation written by the previous
// layout pass. Display is the exception: its own scrollHeight already
// exposes every control, and removing its live allocation can reset the
// user's scroll position while HUD or preset content is settling.
for (const panel of visiblePanels) {
if (!panel.classList.contains('collapsed') && panel !== displayPanel) {
panel.style.removeProperty('--right-panel-allocated-height');
}
}
const gap = parseFloat(getComputedStyle(stack).rowGap) || 0;
const naturalHeight =
visiblePanels.reduce(
(total, panel) =>
total +
// Measuring lifts each opted-in scroller's max-height, which clamps its
// scroll offset; remember the offsets to put back afterwards.
const scrollers = [
...(stack.querySelectorAll?.('[data-rail-scroller]') || []),
]
.map((node) => [node, node.scrollTop || 0])
.filter(([, top]) => top > 0);
const naturalHeights = new Map();
// Remove live height allocations through a synchronous CSS override. Keeping
// the stored properties intact avoids REMOVE/SET churn on unchanged panels.
// Do not toggle layout classes here: the class observer schedules new passes.
stack.setAttribute('data-rail-measuring', '');
try {
for (const panel of measuredPanels) {
naturalHeights.set(
panel,
Math.max(
panel.getBoundingClientRect().height,
panel.scrollHeight || 0,
panel.classList.contains('collapsed') ? 42 : 0,
),
);
}
} finally {
stack.removeAttribute('data-rail-measuring');
for (const [node, top] of scrollers)
if (node.scrollTop !== top) node.scrollTop = top;
}
const gap = parseFloat(getComputedStyle(stack).rowGap) || 0;
const naturalHeight =
measuredPanels.reduce(
(total, panel) => total + naturalHeights.get(panel),
0,
) +
gap * Math.max(0, visiblePanels.length - 1);
gap * Math.max(0, measuredPanels.length - 1);
const layout = resolveHudRailLayout({
viewportHeight,
panelHeight: naturalHeight,
@@ -155,16 +180,16 @@ export function layoutRightPanelRail({
});
if (!layout) return;
const { safeTop, safeBottom, maxHeight: availableHeight } = layout;
const stabilityBand = viewportHeight * 0.01;
const stabilityBand = Math.max(48, viewportHeight * 0.06);
const wasFocused = stack.classList.contains('layout-focus');
const shouldFocus = wasFocused
? naturalHeight > availableHeight - stabilityBand * 2
: naturalHeight > availableHeight - stabilityBand;
? naturalHeight >= availableHeight - stabilityBand
: naturalHeight > availableHeight;
const layoutTop = shouldFocus ? safeTop : layout.top;
const collapsedHeight = visiblePanels.reduce(
(total, panel) =>
panel.classList.contains('collapsed')
? total + panel.getBoundingClientRect().height
? total + naturalHeights.get(panel)
: total,
0,
);
@@ -196,38 +221,34 @@ export function layoutRightPanelRail({
gap * Math.max(0, visiblePanels.length - 1),
);
const expandedHeights = allocatePanelStackHeights({
naturalHeights: expandedPanels.map((panel) =>
Math.max(panel.getBoundingClientRect().height, panel.scrollHeight || 0),
),
naturalHeights: expandedPanels.map((panel) => naturalHeights.get(panel)),
availableHeight: expandedAvailableHeight,
});
const autoCollapseIndices = hud.visible
? panelStackAutoCollapseIndices({
naturalHeights: expandedPanels.map((panel) =>
Math.max(
panel.getBoundingClientRect().height,
panel.scrollHeight || 0,
// Other HUD variants restore automatic collapse at the start of each pass.
// Do not immediately collapse those panels again and schedule a loop.
const autoCollapseIndices =
hud.visible && hud.variant === 'tactical'
? panelStackAutoCollapseIndices({
naturalHeights: expandedPanels.map((panel) =>
naturalHeights.get(panel),
),
),
allocatedHeights: expandedHeights,
collapseLaterPanels: shouldFocus && hud.variant === 'tactical',
})
: [];
if (autoCollapseIndices.length) {
allocatedHeights: expandedHeights,
collapseLaterPanels: shouldFocus && hud.variant === 'tactical',
})
: [];
if (!isCollapseRetry && autoCollapseIndices.length) {
for (const index of autoCollapseIndices) {
const panel = expandedPanels[index];
panel.classList.add('collapsed', 'layout-auto-collapsed');
onCollapse(panel);
}
pendingCollapseRetries.add(stack);
onRetry();
return;
}
// Write-if-changed. This pass runs on the 500 ms stats cadence, and an
// unconditional REMOVE-then-SET of an unchanged allocation is two style
// mutations per tick on `#pp-toggles` (the one panel the measure-strip
// above deliberately skips) — churn that reads as a genuine panel move to
// the world-overlay host's occluder observer and defeats parked-idle
// render savings. Only a real allocation change may touch the attribute.
// Write-if-changed. This pass runs on the 500 ms stats cadence. Rewriting
// an unchanged allocation wakes the world-overlay occluder observer and
// defeats parked-idle render savings. Measurement leaves these values intact.
expandedPanels.forEach((panel, index) => {
const next = `${expandedHeights[index].toFixed(1)}px`;
if (