From 5623d32bda51b4be9dadbbb3140f3f2f0c54ee06 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Tue, 23 Jun 2026 09:17:01 +0200 Subject: [PATCH 1/3] feat(preview): virtualize Book Preview + progress bar + EPUB export hand-off MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hardens the Book Preview (Scrivenings) view to match what the docs already promised but the code never delivered: - Virtualize the section list via @tanstack/react-virtual (was a plain .map rendering every section at once) — keeps long manuscripts responsive. - Replace the broken IntersectionObserver active-chapter highlight (empty-deps effect read an empty ref map on first commit) with scroll-driven tracking computed from the virtualizer window. - Add a reading-progress bar (role=progressbar) the help text already advertised. - Real Export hand-off: an "Export (EPUB)" button navigates to the Export view preset to EPUB via a one-shot transient-store flag (the docs claimed this; it never existed). - Per-section word-count margin annotation in the heading gutter. - Paged reading mode toggle (page-like sheets); virtualization stays on. - Persist font size/family/word-count/TOC/paged prefs to localStorage. i18n: 3 new preview.* keys translated across all 19 locales + bundles rebuilt. Tests: virtualizer jsdom mock; new coverage for progress bar, paged toggle, export hand-off, and pref persistence. Co-Authored-By: Claude Opus 4.8 --- App.tsx | 2 +- app/transientUiStore.ts | 6 + components/BookPreviewView.tsx | 178 ++++++-- contexts/BookPreviewContext.ts | 10 +- graphify-out/GRAPH_REPORT.md | 430 ++++++++++---------- hooks/useBookPreviewView.ts | 113 +++-- hooks/useExportView.ts | 15 +- locales/ar/common.json | 3 + locales/de/common.json | 3 + locales/el/common.json | 3 + locales/en/common.json | 3 + locales/es/common.json | 3 + locales/eu/common.json | 3 + locales/fa/common.json | 3 + locales/fi/common.json | 3 + locales/fr/common.json | 3 + locales/he/common.json | 3 + locales/hu/common.json | 3 + locales/is/common.json | 3 + locales/it/common.json | 3 + locales/ja/common.json | 3 + locales/ko/common.json | 3 + locales/pt/common.json | 3 + locales/ru/common.json | 3 + locales/sv/common.json | 3 + locales/zh/common.json | 3 + public/locales/ar/bundle.json | 3 + public/locales/de/bundle.json | 3 + public/locales/el/bundle.json | 3 + public/locales/en/bundle.json | 3 + public/locales/es/bundle.json | 3 + public/locales/eu/bundle.json | 3 + public/locales/fa/bundle.json | 3 + public/locales/fi/bundle.json | 3 + public/locales/fr/bundle.json | 3 + public/locales/he/bundle.json | 3 + public/locales/hu/bundle.json | 3 + public/locales/is/bundle.json | 3 + public/locales/it/bundle.json | 3 + public/locales/ja/bundle.json | 3 + public/locales/ko/bundle.json | 3 + public/locales/pt/bundle.json | 3 + public/locales/ru/bundle.json | 3 + public/locales/sv/bundle.json | 3 + public/locales/zh/bundle.json | 3 + tests/unit/BookPreviewView.test.tsx | 45 +- tests/unit/hooks/useBookPreviewView.test.ts | 47 ++- 47 files changed, 643 insertions(+), 317 deletions(-) diff --git a/App.tsx b/App.tsx index e1ff3fbdf..3ef59a2e0 100644 --- a/App.tsx +++ b/App.tsx @@ -629,7 +629,7 @@ const App: FC = ({ isNewUser }) => { case 'critic': return ; case 'preview': - return ; + return ; case 'progress': return ; case 'objects': diff --git a/app/transientUiStore.ts b/app/transientUiStore.ts index 9a1c63e11..672e90500 100644 --- a/app/transientUiStore.ts +++ b/app/transientUiStore.ts @@ -39,6 +39,10 @@ interface TransientUiState { // QNBS-v3: Phase 3 — ProForge "Ask Copilot" chip pre-fills the composer without prop-drilling. copilotDraftMessage: string | null; setCopilotDraftMessage: (msg: string | null) => void; + // QNBS-v3: Book Preview → Export hand-off — preselect the Export view's format when arriving + // from the preview's "Export" button, without coupling the two views or a URL query param. + exportInitialFormat: string | null; + setExportInitialFormat: (format: string | null) => void; } export const useTransientUiStore = create((set) => ({ @@ -70,4 +74,6 @@ export const useTransientUiStore = create((set) => ({ setCopilotInsightExpanded: (value) => set({ copilotInsightExpanded: value }), copilotDraftMessage: null, setCopilotDraftMessage: (msg) => set({ copilotDraftMessage: msg }), + exportInitialFormat: null, + setExportInitialFormat: (format) => set({ exportInitialFormat: format }), })); diff --git a/components/BookPreviewView.tsx b/components/BookPreviewView.tsx index 7700a7344..cc867133d 100644 --- a/components/BookPreviewView.tsx +++ b/components/BookPreviewView.tsx @@ -1,16 +1,22 @@ +import { useVirtualizer } from '@tanstack/react-virtual'; import type { FC } from 'react'; -import { useCallback } from 'react'; +import { useCallback, useRef, useState } from 'react'; import { BookPreviewContext, useBookPreviewContext } from '../contexts/BookPreviewContext'; import { useBookPreviewView } from '../hooks/useBookPreviewView'; -import type { StorySection } from '../types'; +import type { StorySection, View } from '../types'; import { EmptyState } from './ui/EmptyState'; import { SectionIcon } from './ui/SectionIcon'; import { Select } from './ui/Select'; // ── TOC Sidebar ────────────────────────────────────────────────────────────── -const TocSidebar: FC = () => { - const { t, sections, isTocOpen, activeId, scrollToSection, toggleToc } = useBookPreviewContext(); +// QNBS-v3: activeId/onScrollTo are component-local (driven by the virtualizer's scroll), not the +// hook — so they're passed as props rather than threaded through the context. +const TocSidebar: FC<{ activeId: string | null; onScrollTo: (id: string) => void }> = ({ + activeId, + onScrollTo, +}) => { + const { t, sections, isTocOpen, toggleToc } = useBookPreviewContext(); if (!isTocOpen) return null; return (