diff --git a/README.md b/README.md
index ecd7090c..7b84b203 100644
--- a/README.md
+++ b/README.md
@@ -13,7 +13,7 @@
-
+
@@ -511,7 +511,7 @@ The Settings → AI panel shows a live GPU status badge with adapter details and
| **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) |
| **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking |
| **i18n** | Custom React Context (`I18nContext.tsx`) | 2937 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence |
-| **Testing** | Vitest 4.x (7356+ tests / 595 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
+| **Testing** | Vitest 4.x (7357+ tests / 595 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy |
| **Visualization** | Force-directed graph | Interactive character relationship network |
| **Desktop** | Tauri v2 | Cross-platform installer; auto-updater via `latest.json` |
@@ -549,7 +549,7 @@ WorldScript-Studio/
│ ├── sw.js # PWA Service Worker
│ └── manifest.json # PWA Web App Manifest v3
├── tests/
-│ ├── unit/ # Vitest unit tests (7356+ tests, 595 files) — count spans tests/, components/, packages/*/tests/, not just this folder
+│ ├── unit/ # Vitest unit tests (7357+ tests, 595 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ │ ├── ai/ # aiSmallModules, aiCoreFallbackPaths
│ │ └── settings/ # WebLlmPanel, AiSections
│ └── e2e/ # Playwright specs + helpers.ts
@@ -711,7 +711,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt
| `scorecard` | weekly + `main` push | OpenSSF Scorecard — SARIF uploaded to GitHub Code Scanning |
**Current test metrics (2026-08-30, source-synchronized; CI remains authoritative for pass/fail):**
-- **7356+ unit tests** across **595 test files** — CI is authoritative for pass/fail
+- **7357+ unit tests** across **595 test files** — CI is authoritative for pass/fail
- Coverage thresholds: lines ≥ 80 · branches ≥ 66 · functions ≥ 72 · statements ≥ 78 — enforced in CI (see Codecov badge for live metrics)
- i18n: **2937 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta)
diff --git a/components/manuscript/ManuscriptEditor.tsx b/components/manuscript/ManuscriptEditor.tsx
index a9deb6a2..7183fb3c 100644
--- a/components/manuscript/ManuscriptEditor.tsx
+++ b/components/manuscript/ManuscriptEditor.tsx
@@ -97,6 +97,7 @@ export const ManuscriptEditor: FC<{ isFocusMode: boolean }> = React.memo(({ isFo
// QNBS-v3: Defer highlight computation so keystroke → textarea updates stay synchronous even for long scenes.
const deferredContent = useDeferredValue(activeSection?.content ?? '');
const isHighlightPending = deferredContent !== (activeSection?.content ?? '');
+ // QNBS-v3: Pending mirror opacity preserves WCAG contrast while retaining the deferred-state cue.
// QNBS-v3 (#341): shared with components/ui/Textarea.tsx and ContextPanel.tsx — the raw enum
// value (e.g. 'custom') is not a valid font-family, and the highlight overlay must render the
@@ -398,7 +399,7 @@ export const ManuscriptEditor: FC<{ isFocusMode: boolean }> = React.memo(({ isFo
ref={highlightRef}
data-testid="manuscript-editor-mirror"
dir={dir}
- className={`absolute inset-0 p-4 sm:p-6 md:p-12 pt-2 leading-relaxed pointer-events-none overflow-auto max-w-3xl mx-auto transition-all duration-500 ${isFocusMode ? 'max-w-4xl pt-12' : ''} ${isHighlightPending ? 'opacity-70' : 'opacity-100'}`}
+ className={`absolute inset-0 p-4 sm:p-6 md:p-12 pt-2 leading-relaxed pointer-events-none overflow-auto max-w-3xl mx-auto transition-all duration-500 ${isFocusMode ? 'max-w-4xl pt-12' : ''} ${isHighlightPending ? 'opacity-75' : 'opacity-100'}`}
style={editorStyles}
aria-hidden="true"
>
@@ -526,4 +527,4 @@ export const ManuscriptEditor: FC<{ isFocusMode: boolean }> = React.memo(({ isFo
);
});
-ManuscriptEditor.displayName = 'ManuscriptEditor';
\ No newline at end of file
+ManuscriptEditor.displayName = 'ManuscriptEditor';
diff --git a/tests/unit/manuscript/ManuscriptEditor.test.tsx b/tests/unit/manuscript/ManuscriptEditor.test.tsx
index 85fe08f6..1931ec80 100644
--- a/tests/unit/manuscript/ManuscriptEditor.test.tsx
+++ b/tests/unit/manuscript/ManuscriptEditor.test.tsx
@@ -8,6 +8,24 @@ import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { beforeEach, describe, expect, it, vi } from 'vitest';
+// QNBS-v3: Retain the previous deferred value so pending-state coverage models React's stale render contract.
+const reactDeferredMock = vi.hoisted(() => ({
+ forceStaleValue: false,
+ previousValue: undefined as unknown,
+}));
+
+vi.mock('react', async () => {
+ const actual = await vi.importActual('react');
+ const useDeferredValue = (value: T): T => {
+ if (reactDeferredMock.forceStaleValue && reactDeferredMock.previousValue !== undefined) {
+ return reactDeferredMock.previousValue as T;
+ }
+ reactDeferredMock.previousValue = value;
+ return value;
+ };
+ return { ...actual, useDeferredValue };
+});
+
// ---------------------------------------------------------------------------
// Mocks
// ---------------------------------------------------------------------------
@@ -180,6 +198,9 @@ describe('ManuscriptEditor', () => {
ltMock.available = false;
ltMock.matches = [];
ltMock.applySuggestion.mockReset();
+ // QNBS-v3: Reset deferred mock state so each test observes an isolated render timeline.
+ reactDeferredMock.forceStaleValue = false;
+ reactDeferredMock.previousValue = undefined;
});
it('shows empty state when no section is selected', () => {
@@ -321,5 +342,20 @@ describe('ManuscriptEditor', () => {
} as unknown as React.UIEvent);
expect(mirror.scrollTop).toBe(360);
});
+
+ // QNBS-v3: A stale mirror must retain content and the WCAG-safe pending opacity during edits.
+ it('applies opacity-75 while retaining stale mirror content during deferred rendering', () => {
+ const { rerender } = render();
+ mockActiveSection = {
+ id: 'sec-1',
+ title: 'Chapter One',
+ content: 'Updated content while the edit settles',
+ };
+ reactDeferredMock.forceStaleValue = true;
+ rerender();
+ const mirror = screen.getByTestId('manuscript-editor-mirror');
+ expect(mirror).toHaveClass('opacity-75');
+ expect(mirror).toHaveTextContent('Hello world teh quick brown fox');
+ });
});
-});
\ No newline at end of file
+});