36e11de332
* docs: design spec for gesture-based brightness control (#3021) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: revise brightness-gesture spec per /autoplan review (#3021) CEO+Design+Eng dual-voice review. Key fixes: capture-phase listener (bubble-phase could not suppress foliate paginator), opt-out toggle, 18px threshold, selection guard, brightness seed race, rAF teardown, e-ink stepped overlay, contrast capsule, perceptual curve reuse, listener-level test harness. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reader): swipe-to-adjust brightness gesture on mobile (#3021) Left-edge vertical swipe adjusts screen brightness on iOS/Android, with a Sun-icon progress overlay. Capture-phase non-passive listener suppresses the foliate paginator / page-flip / UI-toggle handlers; selection guard, strip reservation in scrolled mode, eager brightness seed, rAF throttle + teardown. Opt-out toggle in Settings > Behavior > Device (default on). Perceptual curve shared with the menu slider. Pure-helper + listener-level tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reader): detect brightness-swipe edge by screenX; i18n + shorter label (#3021) On-device fix: paginated mode lays the iframe doc out as wide side-by-side columns, so clientX/documentElement.clientWidth are document coordinates and a left-edge touch on a later page never fell inside the strip (armed stayed false). Detect with screenX against the parent window width, matching usePagination. Also: translate the two new setting strings across all locales and shorten the toggle description to 'Slide along the left edge'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
62 lines
2.1 KiB
TypeScript
62 lines
2.1 KiB
TypeScript
import clsx from 'clsx';
|
|
import React from 'react';
|
|
import { PiSun } from 'react-icons/pi';
|
|
import { valueToPosition } from '@/app/reader/utils/brightnessGesture';
|
|
|
|
interface BrightnessOverlayProps {
|
|
visible: boolean;
|
|
/** Brightness value, 0-1. */
|
|
level: number;
|
|
}
|
|
|
|
const isEink = () =>
|
|
typeof document !== 'undefined' && document.documentElement.getAttribute('data-eink') === 'true';
|
|
|
|
/**
|
|
* Transient brightness indicator shown at the left edge while the swipe gesture
|
|
* adjusts brightness. Its own capsule surface keeps it legible over any book
|
|
* background; on e-ink it snaps (no continuous animation) and quantizes the fill.
|
|
*/
|
|
const BrightnessOverlay: React.FC<BrightnessOverlayProps> = ({ visible, level }) => {
|
|
const eink = isEink();
|
|
const clamped = Math.max(0, Math.min(1, level));
|
|
// Fill height tracks the slider's perceptual position; the label shows the value.
|
|
let fillPercent = valueToPosition(clamped) * 100;
|
|
let valuePercent = Math.round(clamped * 100);
|
|
if (eink) {
|
|
fillPercent = Math.round(fillPercent / 10) * 10;
|
|
valuePercent = Math.round(valuePercent / 10) * 10;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
aria-hidden
|
|
dir='ltr'
|
|
className={clsx(
|
|
'pointer-events-none absolute left-0 top-1/2 z-[15] -translate-y-1/2',
|
|
'not-eink:transition-opacity not-eink:duration-200 motion-reduce:transition-none',
|
|
visible ? 'opacity-100' : 'opacity-0',
|
|
)}
|
|
style={{ marginInlineStart: 'calc(env(safe-area-inset-left) + 12px)' }}
|
|
>
|
|
<div
|
|
className={clsx(
|
|
'eink-bordered flex flex-col items-center gap-2 rounded-full px-2 py-3',
|
|
'bg-base-100/90 not-eink:shadow-md',
|
|
)}
|
|
>
|
|
<PiSun className='text-base-content h-4 w-4' />
|
|
<div className='bg-base-content/20 relative h-40 w-1.5 overflow-hidden rounded-full'>
|
|
<div
|
|
className='bg-base-content absolute bottom-0 left-0 w-full rounded-full'
|
|
style={{ height: `${fillPercent}%` }}
|
|
/>
|
|
</div>
|
|
<span className='text-base-content text-xs tabular-nums'>{valuePercent}</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default BrightnessOverlay;
|