Reader: resizable side panel

Drag the left edge of the verse side panel to resize it (arrow keys too,
Shift for bigger steps; double-click resets). The text reflows to make room,
and the width is remembered on this device. Reuses the Study page's resize
handle; the hook now takes the CSS variable to drive.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-01 16:37:50 -04:00
1 parent cc2a8cd111
commit e5812fd4ee
6 files changed
+47 -11

No files matched your search

+5 -4
View File
@@ -6,7 +6,8 @@ const STEP = 24;
const BIG_STEP = 96;
// containerRef: the element the panel sits at the right edge of (for measuring and clamping).
export function useResizableWidth({ storageKey, min = 320, maxAbsolute = 760, fallback = 448, reserve = 600, containerRef }) {
// cssVar: the custom property on the container that carries the width while dragging.
export function useResizableWidth({ storageKey, min = 320, maxAbsolute = 760, fallback = 448, reserve = 600, containerRef, cssVar = '--study-right' }) {
const maxFor = () => {
const w = containerRef.current?.getBoundingClientRect().width ?? 0;
return w > 0 ? Math.max(min, Math.min(maxAbsolute, w - reserve)) : maxAbsolute;
@@ -51,7 +52,7 @@ export function useResizableWidth({ storageKey, min = 320, maxAbsolute = 760, fa
const right = containerRef.current?.getBoundingClientRect().right ?? window.innerWidth;
const next = clamp(right - e.clientX);
dragRef.current.width = next;
containerRef.current?.style.setProperty('--study-right', `${next}px`); // smooth: no React render per move
containerRef.current?.style.setProperty(cssVar, `${next}px`); // smooth: no React render per move
},
onPointerUp: (e) => {
if (!dragRef.current) return;
@@ -65,9 +66,9 @@ export function useResizableWidth({ storageKey, min = 320, maxAbsolute = 760, fa
const step = e.shiftKey ? BIG_STEP : STEP;
if (e.key === 'ArrowLeft') { e.preventDefault(); commit(clamp(current(panelRef.current) + step)); }
else if (e.key === 'ArrowRight') { e.preventDefault(); commit(clamp(current(panelRef.current) - step)); }
else if (e.key === 'Home' || e.key === 'Escape') { e.preventDefault(); containerRef.current?.style.removeProperty('--study-right'); commit(null); }
else if (e.key === 'Home' || e.key === 'Escape') { e.preventDefault(); containerRef.current?.style.removeProperty(cssVar); commit(null); }
},
onDoubleClick: () => { containerRef.current?.style.removeProperty('--study-right'); commit(null); },
onDoubleClick: () => { containerRef.current?.style.removeProperty(cssVar); commit(null); },
title: 'Drag to resize · double-click to reset',
});
+9
View File
@@ -79,3 +79,12 @@ test('the handle describes itself for assistive technology', () => {
const props = setup().handle();
expect(props).toMatchObject({ role: 'separator', 'aria-orientation': 'vertical', 'aria-label': 'Resize side panel', tabIndex: 0, 'aria-valuenow': 448 });
});
test('a different panel can name its own CSS variable', () => {
const h = setup({ cssVar: '--reader-pane' });
act(() => { down(h); move(h, 1000); });
expect(h.style.setProperty).toHaveBeenLastCalledWith('--reader-pane', '400px');
act(() => up(h));
act(() => { h.handle().onDoubleClick(); });
expect(h.style.removeProperty).toHaveBeenLastCalledWith('--reader-pane');
});