Some checks failed
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Lint, typecheck, test (push) Successful in 3m35s
CI / Build container images (push) Has been skipped
CD / Build and push images (push) Successful in 3m45s
CD / Deploy to Test (push) Successful in 13s
CD / Smoke tests against Test (push) Successful in 1m18s
CD / Promote to Int (push) Successful in 11s
CI / Auth e2e pack (push) Has been cancelled
- SidebarResizer: pointer-drag handle on the sidebar's right edge, keyboard-adjustable (arrows, Home/End), double-click resets to the 16rem default; width clamped to 12-32rem - AppLayout persists the width via usePersistentState (ui.sidebar.width) and sets --sidebar-width inline on .app-body, so collapse/force-hide keep animating from/to the chosen width - localized aria-label (de+en), handle hidden while collapsed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwZ4jR4KFAPvpjWevfUGX1
87 lines
3.0 KiB
TypeScript
87 lines
3.0 KiB
TypeScript
import { useRef } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
|
|
/** Sidebar width bounds and default, in rem (issue #99). */
|
|
export const SIDEBAR_MIN_WIDTH_REM = 12;
|
|
export const SIDEBAR_MAX_WIDTH_REM = 32;
|
|
export const SIDEBAR_DEFAULT_WIDTH_REM = 16;
|
|
|
|
/** Step for keyboard adjustment, in rem. */
|
|
const KEYBOARD_STEP_REM = 1;
|
|
|
|
export function clampSidebarWidth(rem: number): number {
|
|
if (!Number.isFinite(rem)) return SIDEBAR_DEFAULT_WIDTH_REM;
|
|
return Math.min(SIDEBAR_MAX_WIDTH_REM, Math.max(SIDEBAR_MIN_WIDTH_REM, rem));
|
|
}
|
|
|
|
interface SidebarResizerProps {
|
|
widthRem: number;
|
|
onResize: (widthRem: number) => void;
|
|
}
|
|
|
|
/**
|
|
* Drag handle on the sidebar's right edge. Pointer-draggable, keyboard-
|
|
* adjustable (arrow keys, Home/End), double-click resets to the default.
|
|
*/
|
|
export function SidebarResizer({ widthRem, onResize }: SidebarResizerProps): React.JSX.Element {
|
|
const { t } = useTranslation();
|
|
const dragStart = useRef<{ pointerX: number; widthRem: number } | null>(null);
|
|
|
|
function remToPx(): number {
|
|
return parseFloat(getComputedStyle(document.documentElement).fontSize) || 16;
|
|
}
|
|
|
|
function handlePointerDown(event: React.PointerEvent<HTMLDivElement>): void {
|
|
// preventDefault (drag/selection guard) also swallows dblclick, so the
|
|
// double-click reset is detected via the click count here.
|
|
event.preventDefault();
|
|
if (event.detail >= 2) {
|
|
dragStart.current = null;
|
|
onResize(SIDEBAR_DEFAULT_WIDTH_REM);
|
|
return;
|
|
}
|
|
dragStart.current = { pointerX: event.clientX, widthRem };
|
|
event.currentTarget.setPointerCapture(event.pointerId);
|
|
}
|
|
|
|
function handlePointerMove(event: React.PointerEvent<HTMLDivElement>): void {
|
|
if (!dragStart.current) return;
|
|
const deltaRem = (event.clientX - dragStart.current.pointerX) / remToPx();
|
|
onResize(clampSidebarWidth(dragStart.current.widthRem + deltaRem));
|
|
}
|
|
|
|
function handlePointerUp(event: React.PointerEvent<HTMLDivElement>): void {
|
|
dragStart.current = null;
|
|
event.currentTarget.releasePointerCapture(event.pointerId);
|
|
}
|
|
|
|
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>): void {
|
|
let next: number | null = null;
|
|
if (event.key === 'ArrowLeft') next = widthRem - KEYBOARD_STEP_REM;
|
|
else if (event.key === 'ArrowRight') next = widthRem + KEYBOARD_STEP_REM;
|
|
else if (event.key === 'Home') next = SIDEBAR_MIN_WIDTH_REM;
|
|
else if (event.key === 'End') next = SIDEBAR_MAX_WIDTH_REM;
|
|
if (next === null) return;
|
|
event.preventDefault();
|
|
onResize(clampSidebarWidth(next));
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="sidebar-resizer"
|
|
role="separator"
|
|
aria-orientation="vertical"
|
|
aria-label={t('layout.sidebar.resize')}
|
|
aria-valuenow={Math.round(widthRem)}
|
|
aria-valuemin={SIDEBAR_MIN_WIDTH_REM}
|
|
aria-valuemax={SIDEBAR_MAX_WIDTH_REM}
|
|
tabIndex={0}
|
|
onPointerDown={handlePointerDown}
|
|
onPointerMove={handlePointerMove}
|
|
onPointerUp={handlePointerUp}
|
|
onDoubleClick={() => onResize(SIDEBAR_DEFAULT_WIDTH_REM)}
|
|
onKeyDown={handleKeyDown}
|
|
/>
|
|
);
|
|
}
|