// Navigator panel - shows full page with draggable viewport box // Affinity/Photoshop-style mini-map for page navigation // Feature Registration Pattern implementation import type { ReaderContext } from "../core/reader-context"; let context: ReaderContext; export function init(readerContext: ReaderContext): void { context = readerContext; let state: NavigatorState | null = null; context.events.on( "navigator:initialize", (detail: { containerSelector: string; totalPages: number }) => { state = initializeNavigator(detail.containerSelector); state.totalPages = detail.totalPages; }, ); context.events.on( "navigator:update", (detail: { currentPage: number; contentImage?: HTMLImageElement }) => { if (state) { state.currentPage = detail.currentPage; if (detail.contentImage) { state.contentImage = detail.contentImage; updateNavigatorViewport(state); } } }, ); context.events.on("navigator:pan", (detail: { x: number; y: number }) => { if (state) { handleNavigatorPan(state, detail.x, detail.y); } }); } interface NavigatorState { panelId: string; container: HTMLElement; viewport: HTMLElement; currentPage: number; totalPages: number; scale: number; contentImage: HTMLImageElement | null; isDragging: boolean; } function initializeNavigator(containerSelector: string): NavigatorState { const container = document.querySelector(containerSelector) as HTMLElement; if (!container) throw new Error("Navigator container not found"); const viewport = document.createElement("div"); viewport.className = "navigator-viewport-box"; viewport.style.cssText = ` position: absolute; border: 2px solid var(--accent-color, #3b82f6); background: rgba(59, 130, 246, 0.1); cursor: move; z-index: 10; `; container.appendChild(viewport); const state: NavigatorState = { panelId: "navigator", container, viewport, currentPage: 1, totalPages: 1, scale: 0.1, contentImage: null, isDragging: false, }; setupNavigatorDragHandler(state); return state; } function setupNavigatorDragHandler(state: NavigatorState): void { state.viewport.addEventListener("mousedown", (e) => { e.preventDefault(); state.isDragging = true; state.viewport.style.cursor = "grabbing"; }); document.addEventListener("mousemove", (e) => { if (!state.isDragging || !state.contentImage) return; const containerRect = state.container.getBoundingClientRect(); const imgRect = state.contentImage.getBoundingClientRect(); const relX = (e.clientX - imgRect.left) / imgRect.width; const relY = (e.clientY - imgRect.top) / imgRect.height; const mainViewer = document.getElementById("reader-content"); if (mainViewer) { mainViewer.dispatchEvent( new CustomEvent("navigator-pan", { detail: { x: relX, y: relY }, }), ); } }); document.addEventListener("mouseup", () => { if (state.isDragging) { state.isDragging = false; state.viewport.style.cursor = "move"; } }); } function updateNavigatorViewport(state: NavigatorState): void { if (!state.contentImage) return; const containerRect = state.container.getBoundingClientRect(); const imgRect = state.contentImage.getBoundingClientRect(); state.scale = containerRect.width / imgRect.width; state.viewport.style.width = `${containerRect.width}px`; state.viewport.style.height = `${containerRect.height * state.scale}px`; state.viewport.style.left = "0"; state.viewport.style.top = `${(state.currentPage - 1) * imgRect.height * state.scale}px`; } function handleNavigatorPan(state: NavigatorState, x: number, y: number): void { if (!state.contentImage) return; const imgRect = state.contentImage.getBoundingClientRect(); const viewportX = x * state.container.offsetWidth; const viewportY = y * state.container.offsetHeight; state.viewport.style.left = `${viewportX}px`; state.viewport.style.top = `${viewportY}px`; }