feat(right_pane): simplify collapsing mechanism

This commit is contained in:
Elian Doran 2025-12-18 16:15:21 +02:00
parent 7b04ca8cc7
commit 7af063e7cd
No known key found for this signature in database
3 changed files with 10 additions and 143 deletions

View File

@ -1,5 +1,7 @@
body.experimental-feature-new-layout #right-pane { body.experimental-feature-new-layout #right-pane {
width: 300px; width: 300px;
display: flex;
flex-direction: column;
.card { .card {
margin-inline: 0; margin-inline: 0;
@ -23,6 +25,10 @@ body.experimental-feature-new-layout #right-pane {
} }
} }
.card:not(.collapsed) {
flex-grow: 1;
}
.gutter-vertical + .card .card-header { .gutter-vertical + .card .card-header {
padding-top: 0; padding-top: 0;
} }

View File

@ -12,11 +12,6 @@ import HighlightsList from "./HighlightsList";
import TableOfContents from "./TableOfContents"; import TableOfContents from "./TableOfContents";
const MIN_WIDTH_PERCENT = 5; const MIN_WIDTH_PERCENT = 5;
const COLLAPSED_SIZE = 25;
export const RightPanelContext = createContext({
setExpanded(cardEl: HTMLElement, expanded: boolean) {}
});
export default function RightPanelContainer() { export default function RightPanelContainer() {
// Split between right pane and the content pane. // Split between right pane and the content pane.
@ -38,140 +33,11 @@ export default function RightPanelContainer() {
<HighlightsList /> <HighlightsList />
]; ];
// Split between items.
const innerSplitRef = useRef<Split.Instance>(null);
useEffect(() => {
const rightPaneContainer = document.getElementById("right-pane");
const elements = Array.from(rightPaneContainer?.children ?? []) as HTMLElement[];
const splitInstance = Split(elements, {
direction: "vertical",
minSize: COLLAPSED_SIZE,
gutterSize: 4
});
innerSplitRef.current = splitInstance;
return () => splitInstance.destroy();
}, [ items ]);
const sizesBeforeCollapse = useRef(new WeakMap<HTMLElement, number>()); const sizesBeforeCollapse = useRef(new WeakMap<HTMLElement, number>());
return ( return (
<div id="right-pane"> <div id="right-pane">
<RightPanelContext.Provider value={{
setExpanded(cardEl, expanded) {
const splitInstance = innerSplitRef.current;
if (!splitInstance) return;
const rightPaneEl = document.getElementById("right-pane");
const children = Array.from(rightPaneEl?.querySelectorAll(":scope > .card") ?? []);
const pos = children.indexOf(cardEl);
if (pos === -1) return;
const sizes = splitInstance.getSizes(); // percentages
const COLLAPSED_SIZE = 0; // keep your current behavior; consider a small min later
// Choose recipients/donors: nearest expanded panes first; if none, all except pos.
const recipients = getRecipientsByDistance(sizes, pos, COLLAPSED_SIZE);
const fallback = getExpandedIndices(sizes, pos, -Infinity); // all other panes
const targets = recipients.length ? recipients : fallback;
if (!expanded) {
const sizeBeforeCollapse = sizes[pos];
sizesBeforeCollapse.current.set(cardEl, sizeBeforeCollapse);
// Collapse
sizes[pos] = COLLAPSED_SIZE;
// Give freed space to other panes
const freed = sizeBeforeCollapse - COLLAPSED_SIZE;
distributeInto(sizes, targets, freed);
} else {
const want = sizesBeforeCollapse.current.get(cardEl) ?? 50;
// Take space back from other panes to expand this one
const took = takeFrom(sizes, targets, want);
sizes[pos] = COLLAPSED_SIZE + took; // if donors couldn't provide all, expand partially
}
// Optional: tiny cleanup to avoid negatives / floating drift
for (let i = 0; i < sizes.length; i++) sizes[i] = clamp(sizes[i], 0, 100);
// Normalize to sum to 100 (Split.js likes this)
const sum = sizes.reduce((a, b) => a + b, 0);
if (sum > 0) {
for (let i = 0; i < sizes.length; i++) sizes[i] = (sizes[i] / sum) * 100;
}
splitInstance.setSizes(sizes);
}
}}>
{items} {items}
</RightPanelContext.Provider>
</div> </div>
); );
} }
function getExpandedIndices(sizes, skipIndex, COLLAPSED_SIZE) {
const idxs = [];
for (let i = 0; i < sizes.length; i++) {
if (i === skipIndex) continue;
if (sizes[i] > COLLAPSED_SIZE) idxs.push(i);
}
return idxs;
}
// Prefer nearby panes (VS Code-ish). Falls back to "all expanded panes".
function getRecipientsByDistance(sizes, pos, COLLAPSED_SIZE) {
const recipients = [];
for (let d = 1; d < sizes.length; d++) {
const left = pos - d;
const right = pos + d;
if (left >= 0 && sizes[left] > COLLAPSED_SIZE) recipients.push(left);
if (right < sizes.length && sizes[right] > COLLAPSED_SIZE) recipients.push(right);
}
return recipients;
}
// Distribute `amount` into `recipients` proportionally to their current sizes.
function distributeInto(sizes, recipients, amount) {
if (amount === 0 || recipients.length === 0) return;
const total = recipients.reduce((sum, i) => sum + sizes[i], 0);
if (total <= 0) {
// equal split fallback
const delta = amount / recipients.length;
recipients.forEach(i => (sizes[i] += delta));
return;
}
recipients.forEach(i => {
const share = (sizes[i] / total) * amount;
sizes[i] += share;
});
}
// Take `amount` out of `donors` proportionally, without driving anyone below 0.
// Returns how much was actually taken.
function takeFrom(sizes, donors, amount) {
if (amount <= 0 || donors.length === 0) return 0;
// max each donor can contribute (dont go below 0 here; you can change min if you want)
const caps = donors.map(i => ({ i, cap: Math.max(0, sizes[i]) }));
let remaining = amount;
// iterative proportional take with caps
for (let iter = 0; iter < 5 && remaining > 1e-9; iter++) {
const active = caps.filter(x => x.cap > 1e-9);
if (active.length === 0) break;
const total = active.reduce((s, x) => s + sizes[x.i], 0) || active.length;
for (const x of active) {
const weight = total === active.length ? 1 / active.length : (sizes[x.i] / total);
const want = remaining * weight;
const took = Math.min(x.cap, want);
sizes[x.i] -= took;
x.cap -= took;
remaining -= took;
if (remaining <= 1e-9) break;
}
}
return amount - remaining;
}

View File

@ -4,7 +4,6 @@ import { useContext, useRef, useState } from "preact/hooks";
import Icon from "../react/Icon"; import Icon from "../react/Icon";
import { ParentComponent } from "../react/react_utils"; import { ParentComponent } from "../react/react_utils";
import { RightPanelContext } from "./RightPanelContainer";
interface RightPanelWidgetProps { interface RightPanelWidgetProps {
title: string; title: string;
@ -13,7 +12,6 @@ interface RightPanelWidgetProps {
} }
export default function RightPanelWidget({ title, buttons, children }: RightPanelWidgetProps) { export default function RightPanelWidget({ title, buttons, children }: RightPanelWidgetProps) {
const rightPanelContext = useContext(RightPanelContext);
const [ expanded, setExpanded ] = useState(true); const [ expanded, setExpanded ] = useState(true);
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const parentComponent = useContext(ParentComponent); const parentComponent = useContext(ParentComponent);
@ -31,9 +29,6 @@ export default function RightPanelWidget({ title, buttons, children }: RightPane
<Icon <Icon
icon="bx bx-chevron-down" icon="bx bx-chevron-down"
onClick={() => { onClick={() => {
if (containerRef.current) {
rightPanelContext.setExpanded(containerRef.current, !expanded);
}
setExpanded(!expanded); setExpanded(!expanded);
}} }}
/> />
@ -42,9 +37,9 @@ export default function RightPanelWidget({ title, buttons, children }: RightPane
</div> </div>
<div id={parentComponent?.componentId} class="body-wrapper"> <div id={parentComponent?.componentId} class="body-wrapper">
<div class="card-body"> {expanded && <div class="card-body">
{expanded && children} {children}
</div> </div>}
</div> </div>
</div> </div>
); );