The code cited its plan instead of saying why: `E28 review J4`, `(R6 S3b)`, `S1 justified…`,
`deep-review round 5`. That plan lives in the private ops repo, so for every reader of this one the
citation was noise, and a sentence whose subject was the plan step ("round 2 showed…") hid its
lesson behind an id. Each one is gone and the "why" kept: a bare citation is deleted, a sentence
carrying a lesson is reworded around it ("a later attempt showed…").
Measured (`scripts/audit_metrics.py`, product code): 652 lines in 270 files carried an epic code by
the definition the next commit holds (507 / 241 by the old, two-digit-only one) -> 0. Across all
code, tests, scripts and configs: ~1100 epic-code lines plus ~320 sprint/round ids.
Behaviour unchanged, checked mechanically: every changed .py file has the same AST once docstrings
are blanked; css/sh/toml/yml/json files are identical once comments are stripped; the only
non-comment changes in TS/JS are test titles, one description string in a contract table, and five
ESLint messages, whose text is all the gates snapshot diff holds. Not touched: Markdown (docs and
the changelog keep their history), Alembic migrations, TV episode notation (`S2 · E5`), DESIGN.md
ids (`C1`, `B4`, `S3h`), tracker ids (`SEC-16`, `DX-17`).
One stale claim fixed on the way: test_hls_reaper's docstring said the player checkpoints "every
10 s whether or not it is playing", which plex/stream.py calls wrong; it now says what the code does.
58 lines
3.1 KiB
TypeScript
58 lines
3.1 KiB
TypeScript
// docs/DESIGN.md "Text stays selectable": a click that ends a text selection does nothing. The
|
|
// mouseup of a drag-select fires a click on whatever the drag started in, so a title that opens its
|
|
// video on click would play it the moment the reader finished selecting its words to copy them.
|
|
// A plain click collapses any selection on mousedown, so a non-empty selection that touches the
|
|
// element at click time can only be the one this gesture just made.
|
|
|
|
/** True when this click ended a selection: the page's selection is non-empty and starts or ends
|
|
* inside the clicked element. Takes the EVENT, not the element, because a keyboard click (Enter /
|
|
* Space, `detail` 0) ends no selection — it has no pointer — so a title whose words are still
|
|
* selected must open on Enter. */
|
|
export function endsTextSelection(e: { currentTarget: Element; detail: number }): boolean {
|
|
if (e.detail === 0) return false;
|
|
const el = e.currentTarget;
|
|
const sel = typeof window === "undefined" ? null : window.getSelection();
|
|
if (!sel || sel.isCollapsed || !sel.toString().trim()) return false;
|
|
return (
|
|
(!!sel.anchorNode && el.contains(sel.anchorNode)) ||
|
|
(!!sel.focusNode && el.contains(sel.focusNode))
|
|
);
|
|
}
|
|
|
|
// The rule's other half: a press that travelled before its release was a drag (across a row's empty
|
|
// padding, say, where nothing selects), not a click, so it opens nothing either.
|
|
|
|
/** How far, in px, the pointer may move between press and release and still count as a click. */
|
|
const CLICK_SLOP = 6;
|
|
|
|
/** True when a click's pointer moved past the slop since its press at `from`. A keyboard click
|
|
* (`detail` 0) carries no pointer, so it never did. */
|
|
export function movedPastSlop(
|
|
from: { x: number; y: number } | null,
|
|
e: { detail: number; clientX: number; clientY: number },
|
|
): boolean {
|
|
if (!from || e.detail === 0) return false;
|
|
return Math.abs(e.clientX - from.x) > CLICK_SLOP || Math.abs(e.clientY - from.y) > CLICK_SLOP;
|
|
}
|
|
|
|
// The copy half: a right-click on selected words is a request for the browser's own menu (Copy,
|
|
// Search, Translate), not the object's. An app menu there would leave Ctrl+C as the only way to copy
|
|
// what the reader just selected. Only ON the selection: a right-click beside it (the row's picture,
|
|
// its padding) still means the object.
|
|
|
|
/** True when the page's selection is non-empty and the point (x, y) over `target` lies on it. Where
|
|
* the engine lays out ranges (a browser), "on it" is inside one of the selection's line boxes; where
|
|
* it does not (jsdom), it is `target` touching the selection. */
|
|
export function pointOnSelection(target: EventTarget | null, x: number, y: number): boolean {
|
|
const sel = typeof window === "undefined" ? null : window.getSelection();
|
|
if (!sel || sel.isCollapsed || !sel.toString().trim() || !(target instanceof Node)) return false;
|
|
for (let i = 0; i < sel.rangeCount; i++) {
|
|
const range = sel.getRangeAt(i);
|
|
if (!range.intersectsNode(target)) continue;
|
|
if (typeof range.getClientRects !== "function") return true;
|
|
for (const r of range.getClientRects())
|
|
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return true;
|
|
}
|
|
return false;
|
|
}
|