fix: enable floating review panel height resize

This commit is contained in:
akai
2026-07-09 16:30:52 +08:00
parent 0d8b2f9167
commit ebae9103d2
7 changed files with 117 additions and 19 deletions
@@ -39,6 +39,8 @@ import {
const MIN_REVIEW_PANEL_WIDTH = 0.24;
const MAX_REVIEW_PANEL_WIDTH = 0.48;
const MIN_FLOATING_PANEL_HEIGHT = 0.35;
const MAX_FLOATING_PANEL_HEIGHT = 0.92;
const FLOATING_PANEL_MARGIN = 12;
const cnHtmlPurifyOptions = {
@@ -469,9 +471,11 @@ const ReviewPanel: React.FC = () => {
const isPanelVisible = useReviewModeStore((state) => state.isPanelVisible);
const isPanelPinned = useReviewModeStore((state) => state.isPanelPinned);
const panelWidth = useReviewModeStore((state) => state.panelWidth);
const panelHeight = useReviewModeStore((state) => state.panelHeight);
const setPanelVisible = useReviewModeStore((state) => state.setPanelVisible);
const togglePanelPin = useReviewModeStore((state) => state.togglePanelPin);
const setPanelWidth = useReviewModeStore((state) => state.setPanelWidth);
const setPanelHeight = useReviewModeStore((state) => state.setPanelHeight);
const updateRow = useReviewModeStore((state) => state.updateRow);
const setBookData = useReviewModeStore((state) => state.setBookData);
const bookState = useReviewModeStore((state) =>
@@ -588,7 +592,8 @@ const ReviewPanel: React.FC = () => {
[clampPanelPosition],
);
const toPanelWidthPercent = (fraction: number) => `${Math.round(fraction * 10000) / 100}%`;
const toPanelPercent = (fraction: number) => `${Math.round(fraction * 10000) / 100}%`;
const toPanelVh = (fraction: number) => `${Math.round(fraction * 10000) / 100}vh`;
const resizeFloatingPanel = useCallback(
(clientX: number, rightEdge: number) => {
@@ -597,20 +602,34 @@ const ReviewPanel: React.FC = () => {
Math.min(MAX_REVIEW_PANEL_WIDTH, (rightEdge - clientX) / window.innerWidth),
);
const width = fraction * window.innerWidth;
setPanelWidth(toPanelWidthPercent(fraction));
setPanelWidth(toPanelPercent(fraction));
updatePanelPosition({ x: rightEdge - width, y: panelPositionRef.current.y });
},
[setPanelWidth, updatePanelPosition],
);
const { handleResizeStart: handleDockedResizeStart, handleResizeKeyDown: handleDockedResizeKeyDown } =
usePanelResize({
side: 'end',
minWidth: MIN_REVIEW_PANEL_WIDTH,
maxWidth: MAX_REVIEW_PANEL_WIDTH,
getWidth: () => panelWidth,
onResize: setPanelWidth,
});
const resizeFloatingPanelHeight = useCallback(
(clientY: number, topEdge: number) => {
const fraction = Math.max(
MIN_FLOATING_PANEL_HEIGHT,
Math.min(MAX_FLOATING_PANEL_HEIGHT, (clientY - topEdge) / window.innerHeight),
);
setPanelHeight(toPanelVh(fraction));
requestAnimationFrame(() => updatePanelPosition(panelPositionRef.current));
},
[setPanelHeight, updatePanelPosition],
);
const {
handleResizeStart: handleDockedResizeStart,
handleResizeKeyDown: handleDockedResizeKeyDown,
} = usePanelResize({
side: 'end',
minWidth: MIN_REVIEW_PANEL_WIDTH,
maxWidth: MAX_REVIEW_PANEL_WIDTH,
getWidth: () => panelWidth,
onResize: setPanelWidth,
});
const handleResizeStart = (event: React.MouseEvent | React.TouchEvent) => {
if (isPanelPinned || isMobile) {
@@ -661,7 +680,7 @@ const ReviewPanel: React.FC = () => {
const rightEdge =
panel?.getBoundingClientRect().right ||
panelPositionRef.current.x + currentWidth * window.innerWidth;
setPanelWidth(toPanelWidthPercent(nextWidth));
setPanelWidth(toPanelPercent(nextWidth));
updatePanelPosition({
x: rightEdge - nextWidth * window.innerWidth,
y: panelPositionRef.current.y,
@@ -670,6 +689,53 @@ const ReviewPanel: React.FC = () => {
event.stopPropagation();
};
const handleHeightResizeStart = (event: React.MouseEvent | React.TouchEvent) => {
if (isPanelPinned || isMobile) return;
const panel = panelRef.current;
if (!panel) return;
event.preventDefault();
event.stopPropagation();
const topEdge = panel.getBoundingClientRect().top;
document.body.style.pointerEvents = 'none';
document.body.style.userSelect = 'none';
document.documentElement.style.cursor = 'row-resize';
const handleMove = (moveEvent: MouseEvent | TouchEvent) => {
const clientY =
'touches' in moveEvent ? moveEvent.touches[0]?.clientY : (moveEvent as MouseEvent).clientY;
if (typeof clientY === 'number') resizeFloatingPanelHeight(clientY, topEdge);
};
const handleEnd = () => {
document.body.style.pointerEvents = '';
document.body.style.userSelect = '';
document.documentElement.style.cursor = '';
window.removeEventListener('mousemove', handleMove);
window.removeEventListener('mouseup', handleEnd);
window.removeEventListener('touchmove', handleMove);
window.removeEventListener('touchend', handleEnd);
};
window.addEventListener('mousemove', handleMove, { passive: true });
window.addEventListener('mouseup', handleEnd);
window.addEventListener('touchmove', handleMove, { passive: true });
window.addEventListener('touchend', handleEnd);
};
const handleHeightResizeKeyDown = (event: React.KeyboardEvent) => {
if (isPanelPinned || isMobile) return;
if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return;
const currentHeight = parseFloat(panelHeight) / 100;
const nextHeight =
event.key === 'ArrowDown'
? Math.min(MAX_FLOATING_PANEL_HEIGHT, currentHeight + 0.03)
: Math.max(MIN_FLOATING_PANEL_HEIGHT, currentHeight - 0.03);
setPanelHeight(toPanelVh(nextHeight));
requestAnimationFrame(() => updatePanelPosition(panelPositionRef.current));
event.preventDefault();
event.stopPropagation();
};
const moveFloatingPanelToDefault = useCallback(() => {
if (isPanelPinned || isMobile) return;
const panel = panelRef.current;
@@ -721,7 +787,13 @@ const ReviewPanel: React.FC = () => {
useEffect(() => {
if (!isPanelVisible || isMobile || !isPanelPinned) return;
restoreReadingPositionAfterLayoutChange();
}, [isMobile, isPanelPinned, isPanelVisible, panelWidth, restoreReadingPositionAfterLayoutChange]);
}, [
isMobile,
isPanelPinned,
isPanelVisible,
panelWidth,
restoreReadingPositionAfterLayoutChange,
]);
useEffect(() => {
if (isPanelPinned || isMobile) return;
@@ -834,7 +906,8 @@ const ReviewPanel: React.FC = () => {
className={clsx(
'review-panel-container flex min-w-72 select-none flex-col',
isDocked && 'shrink-0',
'full-height font-sans text-base font-normal transition-[padding-top] duration-300 sm:text-sm',
'font-sans text-base font-normal transition-[padding-top] duration-300 sm:text-sm',
(isDocked || isMobile) && 'full-height',
viewSettings?.isEink ? 'bg-base-100' : 'bg-base-200',
appService?.hasRoundedWindow && 'rounded-window-top-right rounded-window-bottom-right',
isDocked ? 'z-20' : 'z-[45] shadow-2xl',
@@ -847,6 +920,11 @@ const ReviewPanel: React.FC = () => {
style={{
width: isMobile ? '100%' : panelWidth,
maxWidth: isMobile ? '100%' : `${MAX_REVIEW_PANEL_WIDTH * 100}%`,
height: !isMobile && !isPanelPinned ? panelHeight : undefined,
maxHeight:
!isMobile && !isPanelPinned
? `calc(100vh - ${FLOATING_PANEL_MARGIN * 2}px)`
: undefined,
position: isMobile ? 'fixed' : isPanelPinned ? 'relative' : 'absolute',
left: !isMobile && !isPanelPinned ? `${panelPosition.x}px` : undefined,
top: !isMobile && !isPanelPinned ? `${panelPosition.y}px` : undefined,
@@ -868,6 +946,19 @@ const ReviewPanel: React.FC = () => {
onTouchStart={handleResizeStart}
onKeyDown={handleResizeKeyDown}
/>
{!isMobile && !isPanelPinned ? (
<div
className='drag-bar absolute inset-x-0 -bottom-2 h-0.5 cursor-row-resize bg-transparent p-2'
role='slider'
tabIndex={0}
aria-label='调整审校面板高度'
aria-orientation='vertical'
aria-valuenow={parseFloat(panelHeight)}
onMouseDown={handleHeightResizeStart}
onTouchStart={handleHeightResizeStart}
onKeyDown={handleHeightResizeKeyDown}
/>
) : null}
<header
className={clsx(
'border-base-300 flex min-h-12 shrink-0 items-center gap-2 border-b px-3 py-2',