feat: Add missing feature files for build

Create panel-dock-system.ts and navigator-panel.ts in features/
directory with proper init() implementations. Copy offline-manager
and reading-speed-tracker to features/ as well.
This commit is contained in:
2026-04-04 14:20:48 -04:00
parent 6649d0526d
commit bb586984d1
4 changed files with 497 additions and 0 deletions
+133
View File
@@ -0,0 +1,133 @@
// 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";
export function init(context: ReaderContext): void {
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`;
}