feat(audio-player): scrub interaction rework — free-flow drag, pixel magnetism, touch pull-away precision (plan §9)
Mouse: drag is free-flow 1:1 (rail only on Shift+drag, now with window from/to edge labels); clicks land with pixel magnetism (MAGNET_RADIUS_PX=10) — the old 5-second threshold was sub-pixel on long books and never fired, which was the 'chapter scrub isn't accurate' bug. Hover inside the radius locks the tooltip to the line with a highlight. Shift+click jumps to the nearest line from anywhere. Touch: drag is an incremental cursor (applyDrag) whose scale tightens continuously as the finger pulls off the bar (dragScale, exponential 10^(-|dy|/80px)) — the rail is the feedback: window shrinks with the zoom (touchRailSpan) and lifts toward the finger (railLiftPx). Dialog retitled 'Shortcuts & interactions' with a Mouse & touch section driven by POINTER_GUIDE (audio-keys.ts). Old seconds-threshold snapToChapter deleted. 133 unit tests; 19 Playwright checks on the KJV (1189ch) and Gardens (34ch) books.
This commit is contained in:
1 parent
1f30602c23
commit
96fd2aad42
6 files changed
+386
-69
No files matched your search
@@ -91,8 +91,8 @@ templ AudioReader(user User, metadata ReaderMetadata) {
|
||||
@click="helpOpen = !helpOpen"
|
||||
class="rounded-full p-2 hover:bg-surface-hover transition-colors"
|
||||
style="color: var(--text-primary); border: 1px solid var(--border); background-color: color-mix(in srgb, var(--bg-secondary) 80%, transparent);"
|
||||
aria-label="Keyboard shortcuts"
|
||||
title="Keyboard shortcuts (?)"
|
||||
aria-label="Shortcuts & interactions"
|
||||
title="Shortcuts & interactions (?)"
|
||||
>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<rect x="2" y="4" width="20" height="16" rx="2"></rect>
|
||||
@@ -203,6 +203,14 @@ templ AudioReader(user User, metadata ReaderMetadata) {
|
||||
></div>
|
||||
</template>
|
||||
</div>
|
||||
<!-- §9 magnet: the line a click would land on (hover
|
||||
inside the pixel radius locks the tooltip here) -->
|
||||
<div
|
||||
x-show="hoverSnapped && !scrubbing"
|
||||
x-cloak
|
||||
class="absolute top-1/2 -translate-x-1/2 -translate-y-1/2 h-4 w-px rounded-full pointer-events-none"
|
||||
:style="'left:' + hoverPct + '%;background-color:var(--accent);'"
|
||||
></div>
|
||||
<input
|
||||
id="audio-scrub"
|
||||
type="range"
|
||||
@@ -225,14 +233,16 @@ templ AudioReader(user User, metadata ReaderMetadata) {
|
||||
<span x-text="hoverSeconds !== null ? fmtTime(hoverSeconds) : ''"></span>
|
||||
<span style="color: var(--text-secondary);" x-text="hoverSeconds !== null ? ' — ' + chapterTitleAt(hoverSeconds) : ''"></span>
|
||||
</div>
|
||||
<!-- Precision rail: while dragging, ±5 minutes at
|
||||
minutes-per-pixel (floats over the transports,
|
||||
page never shifts) -->
|
||||
<!-- Precision rail (§9): Shift+drag on mouse, or a touch
|
||||
drag pulled off the bar — ±5 min or tighter, free
|
||||
mapping inside. Edge labels self-document the window
|
||||
(floats over the transports, page never shifts) -->
|
||||
<div
|
||||
x-show="scrubbing"
|
||||
x-show="railVisible"
|
||||
x-cloak
|
||||
class="absolute top-full mt-1 inset-x-0 h-10 rounded-lg shadow-lg p-1.5 pointer-events-none"
|
||||
class="absolute top-full mt-1 inset-x-0 h-12 rounded-lg shadow-lg p-1.5 pointer-events-none"
|
||||
style="background-color: var(--bg-secondary); border: 1px solid var(--border); z-index: 30;"
|
||||
:style="railLiftY ? 'transform: translateY(' + railLiftY + 'px);' : ''"
|
||||
>
|
||||
<div class="relative h-full">
|
||||
<div class="absolute inset-x-0 top-1/2 h-px" style="background-color: var(--border);"></div>
|
||||
@@ -240,7 +250,9 @@ templ AudioReader(user User, metadata ReaderMetadata) {
|
||||
<div class="absolute top-1 bottom-1 w-px" :style="'left:' + ((tick.start - railFrom) / Math.max(1, railTo - railFrom) * 100) + '%;background-color:var(--text-secondary);opacity:0.5;'"></div>
|
||||
</template>
|
||||
<div class="absolute top-0 bottom-0 w-0.5 -translate-x-1/2 rounded-full" :style="'left:' + railMarkerPct() + '%;background-color:var(--accent);'"></div>
|
||||
<div class="absolute left-1 -bottom-0.5 text-[10px] tabular-nums whitespace-nowrap" style="color: var(--text-secondary);" x-text="fmtTime(scrubValue) + ' — ' + chapterTitleAt(scrubValue)"></div>
|
||||
<div class="absolute left-1 top-0.5 text-[10px] tabular-nums whitespace-nowrap" style="color: var(--text-primary);" x-text="fmtTime(scrubValue) + ' — ' + chapterTitleAt(scrubValue)"></div>
|
||||
<div class="absolute left-1 bottom-0.5 text-[9px] tabular-nums" style="color: var(--text-secondary);" x-text="fmtTime(railFrom)"></div>
|
||||
<div class="absolute right-1 bottom-0.5 text-[9px] tabular-nums" style="color: var(--text-secondary);" x-text="fmtTime(railTo)"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -643,7 +655,7 @@ templ AudioReader(user User, metadata ReaderMetadata) {
|
||||
<div x-show="helpOpen" x-cloak x-transition class="fixed inset-0 z-50 grid place-items-center p-4" @click.outside="helpOpen = false">
|
||||
<div class="w-full max-w-md rounded-2xl shadow-2xl p-4 max-h-[80vh] overflow-y-auto" style="background-color: var(--bg-secondary); border: 1px solid var(--border);">
|
||||
<div class="flex items-center justify-between mb-3 px-1">
|
||||
<span class="text-sm font-semibold" style="color: var(--text-primary);">Keyboard shortcuts</span>
|
||||
<span class="text-sm font-semibold" style="color: var(--text-primary);">Shortcuts & interactions</span>
|
||||
<button type="button" @click="helpOpen = false" class="rounded-full p-1.5 hover:bg-surface-hover" style="color: var(--text-secondary);" aria-label="Close shortcuts">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M18 6 6 18"></path>
|
||||
@@ -661,6 +673,15 @@ templ AudioReader(user User, metadata ReaderMetadata) {
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- Pointer gestures (audio-keys.ts POINTER_GUIDE): same
|
||||
dialog, so every scrub interaction is discoverable -->
|
||||
<div class="mt-3 mb-1 px-2 text-xs font-semibold uppercase tracking-wide" style="color: var(--text-secondary);">Mouse & touch</div>
|
||||
<template x-for="row in pointerGuide" :key="row.label">
|
||||
<div class="flex items-center justify-between gap-3 px-2 py-1.5 rounded-lg">
|
||||
<span class="text-sm" style="color: var(--text-secondary);" x-text="row.description"></span>
|
||||
<kbd class="text-xs px-1.5 py-0.5 rounded tabular-nums shrink-0" style="background-color: var(--surface-hover); color: var(--text-primary); border: 1px solid var(--border);" x-text="row.label"></kbd>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user