Files
InkNote/frontend/src/hooks/useEdgeSwipe.ts
T
marcandClaude Opus 5 9507761bce Colour, a page you can see the edge of, and a document on its own
Five of the nine things on the list, and one finding that came out of them.

Colour: a marker pen and an ink colour, written as marks here rather than
installed — the schema of a note decides what can be stored in it forever. A
highlight is a <mark> so a reader that drops the colour still shows the word
as marked, and marked text is painted dark whatever the theme. Both travel as
HTML in Markdown, like underline, and both are read back.

The PDF fills the screen: the browser's own full screen for the window frame,
an overlay for the sidebar and the list, Escape and a visible button for the
way out.

The dashboard leads with the recent notes, sorted by time whatever order the
notebooks are in. A tap on the blank part of a page puts the caret at the
nearest position instead of nowhere. A drag from the left edge shows the
sidebar. The page has an edge on a phone, where it had none.

The finding: at the speed a robot types, a long note loses keystrokes between
the editor and the pagination measuring after each one — 87 of 220
paragraphs. Nothing is lost at 50 characters a second, so the page-count
check now pastes its bulk text rather than measuring typing throughput. And
the history suite asked for "the first note in the list" without waiting for
the note to reach the server; it asks by name now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 12:40:21 +02:00

91 lines
3.2 KiB
TypeScript

import { useEffect } from "react";
/** How close to the left edge a swipe must start to count as opening one. */
const EDGE_PX = 28;
/** How far it must travel sideways before it is a swipe rather than a tap. */
const TRAVEL_PX = 60;
/** And how straight: past this much vertical movement it is a scroll. */
const SLOPE = 1.2;
interface EdgeSwipe {
enabled: boolean;
isOpen: boolean;
onOpen: () => void;
onClose: () => void;
}
/**
* Dragging in from the left edge shows the sidebar; dragging it back hides it.
*
* The gesture every app on a phone has, and the reason it has to be a gesture
* here too: the button that opens the sidebar sits in the top corner, which is
* the one part of a tall screen a thumb cannot reach.
*
* Three rules keep it from firing by accident, and each is there for something
* that went wrong without it:
*
* • it must **start at the edge**, or every sideways drag inside a note — a
* text selection, a table being scrolled — would open the sidebar;
* • it must travel far enough to be meant, or a tap that slides by two pixels
* counts;
* • it must be more sideways than downwards, or scrolling a long note with a
* thumb near the edge opens it constantly.
*
* Listening on `document` rather than on the sidebar: the gesture starts where
* the sidebar is *not*, which is the whole point of it.
*/
export function useEdgeSwipe({ enabled, isOpen, onOpen, onClose }: EdgeSwipe): void {
useEffect(() => {
if (!enabled) return undefined;
let startX = 0;
let startY = 0;
let tracking = false;
const onStart = (e: TouchEvent) => {
if (e.touches.length !== 1) return; // a pinch is not a swipe
const t = e.touches[0];
startX = t.clientX;
startY = t.clientY;
// Closed: only from the edge. Open: anywhere on the panel, because the
// whole panel is the thing being pushed back.
tracking = isOpen ? startX < window.innerWidth * 0.8 : startX <= EDGE_PX;
};
const onMove = (e: TouchEvent) => {
if (!tracking || e.touches.length !== 1) return;
const t = e.touches[0];
const dx = t.clientX - startX;
const dy = t.clientY - startY;
if (Math.abs(dy) > Math.abs(dx) * SLOPE) {
tracking = false; // a scroll, not a swipe
return;
}
if (!isOpen && dx > TRAVEL_PX) {
tracking = false;
onOpen();
} else if (isOpen && dx < -TRAVEL_PX) {
tracking = false;
onClose();
}
};
const stop = () => {
tracking = false;
};
// Passive: this never calls preventDefault — the page must keep scrolling
// normally while the gesture is being judged.
document.addEventListener("touchstart", onStart, { passive: true });
document.addEventListener("touchmove", onMove, { passive: true });
document.addEventListener("touchend", stop, { passive: true });
document.addEventListener("touchcancel", stop, { passive: true });
return () => {
document.removeEventListener("touchstart", onStart);
document.removeEventListener("touchmove", onMove);
document.removeEventListener("touchend", stop);
document.removeEventListener("touchcancel", stop);
};
}, [enabled, isOpen, onOpen, onClose]);
}