v0.6: Mobile-Touch-Patches für obv

Drei Probleme, drei Fixes:

1. Touch-Drag auf Graph geht nicht:
   obv's mousedown/mousemove/mouseup sind Mausevents only.
   Fix: zusätzlich touchstart/touchmove/touchend/touchcancel
   + getPos() Helper der touches[0] oder clientX nutzt
   + preventDefault() auf Drag (sonst versucht iOS zu scrollen)

2. Suche und Content-Scroll blockiert:
   obv's animateGraph() läuft mit requestAnimationFrame endlos
   und blockt den Main-Thread auf iOS. Plus iOS-300ms-Tap-Delay
   auf inline onclick=.
   Fix:
   - document.hidden check (skip rendering wenn Tab im Hintergrund)
   - autoRotate pausiert 3s nach User-Interaktion
   - html, body { touch-action: manipulation } (entfernt 300ms delay)
   - -webkit-tap-highlight-color: transparent (entfernt blauen Flash)
   - .vault-content, .vault-sidebar { touch-action: pan-y, -webkit-overflow-scrolling: touch }
   - .vault-graph canvas { touch-action: none } (damit Browser-Default Three.js-Drag nicht blockt)

Nicht angefasst: obv's Inline-onclick-Handler (Suche-Eingabe, File-Click,
WikiLinks) — diese funktionieren sobald der 300ms-Tap-Delay weg ist
und der Main-Thread nicht mehr blockt.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-19 04:32:19 +00:00
co-authored by Claude
parent 5394f7e9bf
commit e57b0bc784
+60 -15
View File
@@ -13,7 +13,8 @@
--accent: #FFD700; --link: #FFD700; --border: #313244;
--green: #a6e3a1; --red: #f38ba8; --blue: #89b4fa; --teal: #94e2d5;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
* { margin: 0; padding: 0; box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body { touch-action: manipulation; }
body { background: var(--bg-primary); color: var(--text-primary); font-family: -apple-system, 'Segoe UI', Inter, sans-serif; height: 100vh; overflow: hidden; }
.vault-app {
@@ -47,7 +48,8 @@ body { background: var(--bg-primary); color: var(--text-primary); font-family: -
/* Sidebar file tree */
.vault-sidebar {
background: var(--bg-secondary); border-right: 1px solid var(--border);
overflow-y: auto; padding: 8px 0; font-size: 13px;
overflow-y: auto; -webkit-overflow-scrolling: touch;
touch-action: pan-y; padding: 8px 0; font-size: 13px;
}
.tree-folder { cursor: pointer; user-select: none; }
.tree-folder-header {
@@ -68,7 +70,8 @@ body { background: var(--bg-primary); color: var(--text-primary); font-family: -
/* Content area */
.vault-content {
overflow-y: auto; padding: 32px 48px; max-width: 900px;
overflow-y: auto; -webkit-overflow-scrolling: touch;
touch-action: pan-y; padding: 32px 48px; max-width: 900px;
line-height: 1.7; font-size: 15px;
}
.vault-content h1 { color: var(--accent); font-size: 28px; margin: 24px 0 12px; }
@@ -94,9 +97,9 @@ body { background: var(--bg-primary); color: var(--text-primary); font-family: -
/* Graph panel */
.vault-graph {
background: var(--bg-secondary); border-left: 1px solid var(--border);
position: relative; overflow: hidden;
position: relative; overflow: hidden; touch-action: none;
}
.vault-graph canvas { width: 100% !important; height: 100% !important; }
.vault-graph canvas { width: 100% !important; height: 100% !important; touch-action: none; }
/* Search results overlay */
#search-results {
@@ -321,23 +324,65 @@ async function loadGraph() {
graphScene.add(new THREE.Line(geo, mat));
});
// Orbit
// Orbit (mouse + touch support)
let isDrag = false, lastX = 0, lastY = 0, rotX = 0, rotY = 0;
canvas.addEventListener('mousedown', e => { isDrag = true; lastX = e.clientX; lastY = e.clientY; });
canvas.addEventListener('mousemove', e => {
function getPos(e) {
if (e.touches && e.touches.length) {
return { x: e.touches[0].clientX, y: e.touches[0].clientY };
}
return { x: e.clientX, y: e.clientY };
}
function onDragStart(e) {
e.preventDefault();
isDrag = true;
const p = getPos(e);
lastX = p.x; lastY = p.y;
}
function onDragMove(e) {
if (!isDrag) return;
rotY += (e.clientX - lastX) * 0.005;
rotX += (e.clientY - lastY) * 0.005;
lastX = e.clientX; lastY = e.clientY;
});
canvas.addEventListener('mouseup', () => isDrag = false);
canvas.addEventListener('wheel', e => {
e.preventDefault();
const p = getPos(e);
rotY += (p.x - lastX) * 0.005;
rotX += (p.y - lastY) * 0.005;
lastX = p.x; lastY = p.y;
}
function onDragEnd(e) {
if (e && e.preventDefault) e.preventDefault();
isDrag = false;
}
function onWheel(e) {
graphCamera.position.z += e.deltaY * 0.5;
graphCamera.position.z = Math.max(50, Math.min(800, graphCamera.position.z));
});
}
canvas.addEventListener('mousedown', onDragStart);
canvas.addEventListener('mousemove', onDragMove);
window.addEventListener('mouseup', onDragEnd);
canvas.addEventListener('touchstart', onDragStart, { passive: false });
canvas.addEventListener('touchmove', onDragMove, { passive: false });
canvas.addEventListener('touchend', onDragEnd, { passive: false });
canvas.addEventListener('touchcancel', onDragEnd, { passive: false });
canvas.addEventListener('wheel', onWheel, { passive: true });
// Auto-rotate: when user is not interacting, slow rotation.
// Pauses when tab is hidden, when user is dragging, and after page visibility loss.
let autoRotate = true;
let lastInteractionTime = 0;
const AUTO_ROTATE_RESUME_MS = 3000; // resume auto-rotation 3s after user stops dragging
function animateGraph() {
requestAnimationFrame(animateGraph);
if (document.hidden) return; // skip rendering when tab is in background
const now = Date.now();
const userJustInteracted = (now - lastInteractionTime) < AUTO_ROTATE_RESUME_MS;
if (isDrag) {
lastInteractionTime = now;
autoRotate = false;
} else if (!userJustInteracted) {
autoRotate = true;
}
if (autoRotate && !isDrag) {
rotY += 0.002;
}
graphScene.rotation.y = rotY;
graphScene.rotation.x = rotX;
graphRenderer.render(graphScene, graphCamera);