Swipe Navigation
This commit is contained in:
parent
3d94a298b8
commit
3074d6dd8d
|
|
@ -0,0 +1,65 @@
|
||||||
|
import type { RefObject } from "preact";
|
||||||
|
import { useEffect, useRef } from "preact/hooks";
|
||||||
|
|
||||||
|
const EDGE_THRESHOLD = 30; // px from screen edge to start a swipe
|
||||||
|
const MIN_SWIPE = 50; // minimum horizontal distance to trigger
|
||||||
|
|
||||||
|
export function useSwipeNavigation(
|
||||||
|
elementRef: RefObject<HTMLElement>,
|
||||||
|
onSwipeLeft: () => void,
|
||||||
|
onSwipeRight: () => void,
|
||||||
|
) {
|
||||||
|
// Keep callbacks in a ref so the effect doesn't re-run when they change
|
||||||
|
const callbacksRef = useRef({ onSwipeLeft, onSwipeRight });
|
||||||
|
callbacksRef.current = { onSwipeLeft, onSwipeRight };
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = elementRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
let start: { x: number; y: number } | null = null;
|
||||||
|
|
||||||
|
const onTouchStart = (e: TouchEvent) => {
|
||||||
|
const t = e.touches[0];
|
||||||
|
const fromEdge = t.clientX < EDGE_THRESHOLD
|
||||||
|
|| t.clientX > window.innerWidth - EDGE_THRESHOLD;
|
||||||
|
start = fromEdge ? { x: t.clientX, y: t.clientY } : null;
|
||||||
|
if (fromEdge) {
|
||||||
|
e.preventDefault(); // Prevent scrolling on swipe
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTouchMove = (e: TouchEvent) => {
|
||||||
|
if (!start) return;
|
||||||
|
const t = e.touches[0];
|
||||||
|
const dx = Math.abs(t.clientX - start.x);
|
||||||
|
const dy = Math.abs(t.clientY - start.y);
|
||||||
|
// Prevent Chrome's back/forward navigation once we're sure it's horizontal
|
||||||
|
if (dx > dy && dx > 10) e.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onTouchEnd = (e: TouchEvent) => {
|
||||||
|
if (!start) return;
|
||||||
|
const t = e.changedTouches[0];
|
||||||
|
const dx = t.clientX - start.x;
|
||||||
|
const dy = t.clientY - start.y;
|
||||||
|
start = null;
|
||||||
|
|
||||||
|
if (Math.abs(dx) < MIN_SWIPE || Math.abs(dx) < Math.abs(dy)) return;
|
||||||
|
|
||||||
|
e.preventDefault(); // prevent Chrome's back/forward navigation
|
||||||
|
if (dx > 0) callbacksRef.current.onSwipeRight();
|
||||||
|
else callbacksRef.current.onSwipeLeft();
|
||||||
|
};
|
||||||
|
|
||||||
|
el.addEventListener('touchstart', onTouchStart, { passive: false });
|
||||||
|
el.addEventListener('touchmove', onTouchMove, { passive: false });
|
||||||
|
el.addEventListener('touchend', onTouchEnd, { passive: false });
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
el.removeEventListener('touchstart', onTouchStart);
|
||||||
|
el.removeEventListener('touchmove', onTouchMove);
|
||||||
|
el.removeEventListener('touchend', onTouchEnd);
|
||||||
|
};
|
||||||
|
}, [elementRef]);
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { highlight } from "@common/highlight";
|
import { highlight } from "@common/highlight";
|
||||||
import { useMediaQuery } from "@common/hooks/useMediaQuery";
|
import { useMediaQuery } from "@common/hooks/useMediaQuery";
|
||||||
|
import { useSwipeNavigation } from "@common/hooks/useSwipeNavigation";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { BookMarked, BookOpen, BrainCircuit, Code, FileText, Globe, Layers, List, MapPin, MessageSquare, MessagesSquare, Users, type LucideIcon } from "lucide-preact";
|
import { BookMarked, BookOpen, BrainCircuit, Code, FileText, Globe, Layers, List, MapPin, MessageSquare, MessagesSquare, Users, type LucideIcon } from "lucide-preact";
|
||||||
import { useEffect, useMemo, useRef } from "preact/hooks";
|
import { useEffect, useMemo, useRef } from "preact/hooks";
|
||||||
|
|
@ -62,6 +63,7 @@ export const Editor = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
const editorRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const promptPreview = useMemo(() => {
|
const promptPreview = useMemo(() => {
|
||||||
const text = Prompt.formatSystemPrompt(appState);
|
const text = Prompt.formatSystemPrompt(appState);
|
||||||
|
|
@ -93,6 +95,20 @@ export const Editor = () => {
|
||||||
|
|
||||||
const isMobile = useMediaQuery(`(max-width: ${MOBILE_BREAKPOINT}px)`);
|
const isMobile = useMediaQuery(`(max-width: ${MOBILE_BREAKPOINT}px)`);
|
||||||
|
|
||||||
|
useSwipeNavigation(
|
||||||
|
editorRef,
|
||||||
|
() => { // swipe left → next tab
|
||||||
|
const idx = tabs.findIndex(t => t.id === currentTab);
|
||||||
|
const next = tabs[idx + 1];
|
||||||
|
if (next) dispatch({ type: 'SET_CURRENT_TAB', tab: next.id });
|
||||||
|
},
|
||||||
|
() => { // swipe right → prev tab
|
||||||
|
const idx = tabs.findIndex(t => t.id === currentTab);
|
||||||
|
const prev = tabs[idx - 1];
|
||||||
|
if (prev) dispatch({ type: 'SET_CURRENT_TAB', tab: prev.id });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const hasSelection = currentWorld !== null;
|
const hasSelection = currentWorld !== null;
|
||||||
const isChatOnly = currentWorld?.chatOnly ?? false;
|
const isChatOnly = currentWorld?.chatOnly ?? false;
|
||||||
|
|
||||||
|
|
@ -117,7 +133,7 @@ export const Editor = () => {
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class={styles.editor}>
|
<div class={styles.editor} ref={editorRef}>
|
||||||
{titleBar}
|
{titleBar}
|
||||||
<div class={clsx(
|
<div class={clsx(
|
||||||
styles.content,
|
styles.content,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue