Marginalia resize round (owner): graceful shrink + phantom-fold mute
Three fixes from the owner's resize report: - Continuous shrink: the marginalia now mounts whenever a question owns the band and its box width is pure CSS, so it shrinks continuously as the display narrows. Below MARGIN_MIN_W (220, the owner's floor) it FADES over the normal 300ms transition instead of hard-unmounting; widening past the floor fades it back in. No mid-question unmount, no pop, no invisible limbo. - Live flank tracking: a ResizeObserver on the dock block replaces the render-time measurement — the fade floor reacts instantly at any width (window resize, sidebar toggle, scrollbar changes). The old measurement went stale during drag-resizes (nothing re-rendered on macOS until the question rewrapped), so the mount decision popped late and left a sub-floor sliver visible. - Phantom-fold mute: resizing rewraps the question text, the browser re-clamps scrollTop and emits the clamp as a scroll event — the fold machine read the transient position and flickered the box to the bar and back. Scroll events within 400ms of a WIDTH change are now ignored (width-only: stamping the dock's own height morphs self-muted the machine and swallowed the at-bottom auto re-expand, leaving the box stuck collapsed — caught in live verification). Scroll-up/scroll-down fold behavior itself unchanged (verified live: mute-window scroll ignored, post-mute scroll folds, bottom re-expand works). Gates: typecheck, lint, 423/423 tests.
This commit is contained in:
+49
-16
@@ -44,11 +44,14 @@ const DOCK = "mx-auto w-full max-w-[46rem] px-4";
|
||||
// The DOCK container's fixed width (46rem) and the plan marginalia's
|
||||
// geometry (owner, plan-placement round): the bare diagram sits in the
|
||||
// dock band's RIGHT flank — MARGIN_GAP from the DOCK container's edge and
|
||||
// the same from the column's right edge. Rendered only when the flank
|
||||
// holds a real diagram (no slivers); otherwise the top-bar chip alone.
|
||||
// the same from the column's right edge. The box width is pure CSS, so
|
||||
// the diagram shrinks CONTINUOUSLY with the flank; MARGIN_MIN_W is only
|
||||
// the fade floor (owner, resize round) — below it the marginalia fades
|
||||
// out over the normal transition instead of popping away, and it stays
|
||||
// mounted while the question owns the band so widening fades it back.
|
||||
const DOCK_W = 46 * 16;
|
||||
const MARGIN_GAP = 16;
|
||||
const MARGIN_MIN_W = 240;
|
||||
const MARGIN_MIN_W = 220;
|
||||
// The dock overlay's gradient band (h-24) — part of the reserved tail.
|
||||
const GRADIENT_H = 96;
|
||||
// Fold thresholds (owner, scroll round 1, tuned round 3): pixel distance
|
||||
@@ -810,21 +813,40 @@ function DockOverlay({
|
||||
|
||||
// The plan marginalia's flank width (owner, plan-placement round): from
|
||||
// the DOCK container's right edge to the block's right edge — the bare
|
||||
// diagram's canvas. Re-measured every render (cheap offsetWidth read):
|
||||
// window resizes flow through sbInset's listener, and the sidebar toggle
|
||||
// re-renders the column without any event at all. Below MARGIN_MIN_W the
|
||||
// margin is no margin — the chip alone carries the plan.
|
||||
// diagram's canvas. Tracked LIVE by a ResizeObserver on the block
|
||||
// (owner, resize round): every geometry change — window resize, sidebar
|
||||
// toggle, scrollbar appearance, the box's own rewrap — reports the real
|
||||
// width instantly, so the fade floor never lags behind reality (the old
|
||||
// render-time measurement went stale during drag-resizes and the mount
|
||||
// decision popped late).
|
||||
const [flankW, setFlankW] = useState(0);
|
||||
const flankWRef = useRef(0);
|
||||
// Part A of the same round (the resize mute): any block size change
|
||||
// stamps this — the fold machine ignores scroll events for a beat after,
|
||||
// because a resize/rewrap re-clamps scrollTop and emits PHANTOM scroll
|
||||
// events mid-transient (the box flickered to the bar and back).
|
||||
const geometryAtRef = useRef(0);
|
||||
useIsoLayoutEffect(() => {
|
||||
const block = blockRef.current;
|
||||
if (!block) return;
|
||||
const next = Math.round(block.offsetWidth / 2 - DOCK_W / 2 - 2 * MARGIN_GAP);
|
||||
if (Math.abs(next - flankWRef.current) > 1) {
|
||||
flankWRef.current = next;
|
||||
setFlankW(next);
|
||||
}
|
||||
});
|
||||
const measure = () => {
|
||||
const next = Math.round(block.offsetWidth / 2 - DOCK_W / 2 - 2 * MARGIN_GAP);
|
||||
// Stamp the mute on WIDTH changes ONLY: column resizes are the
|
||||
// phantom-scroll source. Height changes are the dock's own doing
|
||||
// (morphs, question rewraps) — stamping those would self-mute the
|
||||
// fold machine and swallow the at-bottom auto re-expand right
|
||||
// after a fold (verified live: the box got stuck collapsed).
|
||||
if (Math.abs(next - flankWRef.current) > 1) {
|
||||
flankWRef.current = next;
|
||||
setFlankW(next);
|
||||
geometryAtRef.current = Date.now();
|
||||
}
|
||||
};
|
||||
measure();
|
||||
const ro = new ResizeObserver(measure);
|
||||
ro.observe(block);
|
||||
return () => ro.disconnect();
|
||||
}, []);
|
||||
|
||||
// The overlay must not paint over the scroller's scrollbar (owner, scroll
|
||||
// round 2): the scroller reserves symmetric gutters (scrollbar-gutter:
|
||||
@@ -908,6 +930,11 @@ function DockOverlay({
|
||||
if (!el) return;
|
||||
const onScroll = () => {
|
||||
if (!pendingRef.current) return;
|
||||
// Resize mute (owner, resize round): a geometry change re-clamps
|
||||
// scrollTop and the browser emits the clamp as a scroll event —
|
||||
// reading the transient position folded the box to the bar for a
|
||||
// flicker. Real scrolling resumes after the brief mute window.
|
||||
if (Date.now() - geometryAtRef.current < 400) return;
|
||||
const d = el.scrollHeight - el.clientHeight - el.scrollTop;
|
||||
if (expandedRef.current) {
|
||||
if (d > COLLAPSE_DIST && !expandIntentRef.current) setExpanded(false);
|
||||
@@ -1009,8 +1036,12 @@ function DockOverlay({
|
||||
of the lesson's page rather than a floating widget. Mounted only
|
||||
while a question owns the band (its reserved height is the canvas);
|
||||
folding the dock fades + blurs it out of existence, so reading
|
||||
the transcript in full wipes the margin clean. Click → popup. */}
|
||||
{pending && plan && flankW >= MARGIN_MIN_W && (
|
||||
the transcript in full wipes the margin clean. Click → popup.
|
||||
Width (owner, resize round): the box shrinks continuously with
|
||||
the flank; below MARGIN_MIN_W it FADES (never pops away — no
|
||||
mid-question unmount), and widening past the floor fades it
|
||||
back in. */}
|
||||
{pending && plan && (
|
||||
<button
|
||||
type="button"
|
||||
data-testid="plan-marginalia"
|
||||
@@ -1018,7 +1049,9 @@ function DockOverlay({
|
||||
title="Open the full plan"
|
||||
style={{ left: `calc(50% + ${DOCK_W / 2 + MARGIN_GAP}px)` }}
|
||||
className={`absolute bottom-5 right-4 top-0 cursor-pointer transition-[filter,opacity] duration-300 ease-out ${
|
||||
expanded ? "pointer-events-auto opacity-100 blur-0" : "pointer-events-none opacity-0 blur-md"
|
||||
expanded && flankW >= MARGIN_MIN_W
|
||||
? "pointer-events-auto opacity-100 blur-0"
|
||||
: "pointer-events-none opacity-0 blur-md"
|
||||
}`}
|
||||
>
|
||||
{plan}
|
||||
|
||||
Reference in New Issue
Block a user