Skip to content

Mundus v0.7.98

Choose a tag to compare

@NotBedd NotBedd released this 21 Oct 15:44
· 52 commits to main since this release

WORKLOG — Mundus v0.7.98 (v0.7.97 included)

  1. GitHub Actions build loop + everything it needs
  2. investigate & fix the "stuck at end of a page" scroll bug
  3. Character-name auto-highlight must not briefly disappear while the spelling check runs.
  4. Story flow graph: documents with draft status must show a grey dot (green/yellow for completed/in-review were reported as already correct).
  5. Sync status hover tooltip: show both the current Mundus's storage and the app-wide total (all Mundus combined).

1. GitHub build loop

  • .github/workflows/build.yml (new): tag trigger v* + workflow_dispatch; matrix windows-latest / ubuntu-latest / macos-latest; steps: checkout@v4 → setup-node@v4 (Node 22, npm cache) → npm ci → npm run build → npx electron-builder --publish never (targets already fully configured in electron-builder.json: NSIS+portable / AppImage+deb+rpm / dmg+zip) → artifacts per OS; release job (tag builds only) downloads artifacts and publishes a GitHub Release via softprops/action-gh-release@v2 with auto release notes. CSC_IDENTITY_AUTO_DISCOVERY=false (unsigned by design); permissions: contents: write.
  • .gitignore (rewritten): ignores node_modules/dist/dist-electron/release/coverage/tmp/logs/OS noise + secrets (.env*, firebase-applet-config.json — preserved from the original file). build/ deliberately kept tracked (icons + appstream required by electron-builder in CI — the original .gitignore wrongly ignored it).

2. Scroll-stuck bug — root cause + fix

Symptom: sometimes, at the end of a page, scrolling up bounces back down; re-clicking the document in the sidebar fixes it.

Root cause chain (all found in src/components/TextEditor.tsx):

  1. The manuscript scroller had onScroll={updatePageStats} — every scroll event ran the full pagination engine (normalizeDocumentPages).
  2. That engine mutates layout: strips every page-start block's virtual-gap margin-top (pre-pass), re-measures blocks (getBoundingClientRect / zoom-scale), re-applies huge gap margins on page-start blocks.
  3. Mutations while the viewport sits at a page end trigger Chrome scroll anchoring, which re-adjusts scrollTop toward the (moving) page-start block → the adjustment fires another scroll event → step 1 again → a mutation → anchoring → scroll feedback loop that traps the view at the page end. Intermittency explained: interior pages recompute stably (no mutation → no anchoring); boundary blocks can flip-flop on sub-pixel/rounding noise.
  4. A secondary amplifier: a ResizeObserver on the editor re-runs pagination whenever the pagination itself resizes the editor (boundary flip-flop ⇒ layout thrash).

Fix (3 layers):

  • A. onScroll no longer paginates — replaced with new updateCurrentPageFromScroll(): a read-only handler that refreshes the "page N of M" indicator from totalPagesRef + scrollTop (same math as the full pass, functional setPageStats that no-ops when unchanged). Pagination now happens only on input/save/resize/doc-switch/page-format paths, as intended.
  • B. #editor-scroll-area { overflow-anchor: none; } (index.css) — opts the scroller out of Chrome scroll anchoring entirely; any legitimate mid-scroll mutation (autosave, image insert) can no longer yank the viewport. No-op where unsupported (Safari).
  • C. Oscillation damper — normalizeDocumentPages now reports whether it actually mutated DOM (paginationMutatedRef); the ResizeObserver tracks a mutating-streak (4 passes within 800ms ⇒ 600ms cooldown). Direct input/save pagination bypasses the damper, so typing responsiveness is untouched.

3. Character highlight flicker during spellcheck — root cause + fix

Root cause (src/components/TextEditor.tsx, character-name highlight effect):

  • The effect's dependency-change cleanup deleted every registered highlight AND the shared <style> tag immediately, while re-application waited behind the existing 220 ms debounce. Every re-run therefore blacked the highlights out for ≥220 ms.
  • Re-run triggers in normal use: a keystroke (document.plainText), any pagination childList mutation (domStructureEpoch — bumped by the v0.7.83 observer after autosave/pagination), and character-card edits (characters). The spellcheck rescan (250 ms), squiggle re-render (~400 ms) and this blackout (0–220 ms) all land in the same post-typing window — which is why users perceived it as "the highlight disappears while the spelling check is being run".
  • Confirmed NOT the cause: currentMundusCharacters is useMemo-stable in App.tsx, so ordinary re-renders (syncStatus flips etc.) never re-triggered the effect — the blackout came purely from the nuke-then-debounce pattern.

Fix (atomic swap instead of nuke → debounce gap):

  • The dependency-change cleanup now only cancels the pending re-scan timer. Existing highlights stay painted (live Ranges keep tracking their text nodes through edits), and the debounced pass swaps old registrations for new ones inside a single synchronous task (delete + highlights.set + style update) — no paint can ever show the gap.
  • Real teardown moments keep immediate removal: feature toggle-off (branch), document switch (new charHighlightPrevDocIdRef check drops the previous document's registrations at once), and unmount (new dedicated useEffect(() => () => removeAllCharHighlightAssets(), [])).
  • Teardown logic hoisted to a module-level removeAllCharHighlightAssets() so the effect body and the unmount cleanup share one implementation.
  • Side benefit: highlights no longer blink during typing itself either (they used to vanish from the first keystroke until 220 ms after the last one).
  • Untouched on purpose: the spellcheck squiggle effect (its cleanup already never deleted spell-errors, which is why squiggles never flickered) and both debounce durations.

4. Status dots on the story flow graph (and timeline)

Root cause: the canonical status scheme lives in the editor's status picker (TextEditor.tsx): complete → emerald, revision ("in review") → amber, draft → neutral grey. The story-flow node dot only had a two-way mapping — doc.status === 'draft' ? 'bg-amber-400' : 'bg-emerald-400' — so drafts showed yellow and in-review scenes showed green (indistinguishable from completed).

Fix:

  • StoryFlowGraph.tsx node dot → three-way mapping matching the editor exactly: complete ? 'bg-emerald-500' : revision ? 'bg-amber-500' : 'bg-neutral-400' (dot size/title={doc.status} untouched; the now-unused isDraft const removed).
  • Same latent bug fixed in TimelineView.tsx (scene-timeline lane cards used the identical two-way mapping with rings) — kept the ring styling, now emerald-500/amber-500/neutral-400. (Beyond the letter of the request; same defect, fixed for consistency — flagging it explicitly.)

5. Storage tooltip — current Mundus + app-wide total

Root cause/finding: the sidebar's storage approximation only received documents/folders already filtered to the active Mundus (currentMundusDocs/currentMundusFolders in App.tsx). All projects actually share single storage keys (mundus_documents / mundus_folders, each row tagged mundusId), and App state holds the unfiltered lists — so the total was available without any extra reads.

Fix:

  • App.tsx passes two new optional props to : allDocuments={documents} and allFolders={folders} (no new storage scans, no async).
  • Sidebar.tsx recomputes both scopes with the same byte formula as before (content + plainText + title + 180 B/doc, +90 B/folder, +16 KB base): "This Mundus" keeps the per-project number; "All Mundus" sums every project. Props fall back to the scoped lists when omitted, so any other caller stays valid.
  • Tooltip (both the expanded-sidebar hover and the collapsed-sidebar portal copy) now reads:
    • header "Cloud Storage" + percent — percent now tracks the app-wide total (that's what counts against the Firestore free tier);
    • the progress bar is now live (width: storageBarPercent, clamped 1.5–100%) instead of the old hardcoded w-[1.5%];
    • rows: This Mundus: X / All Mundus: Y + Quota: 1.0 GB;
    • the offline (Firebase unconfigured) branch shows the same two rows.
  • Scope note (by design): the number stays the app's existing approximation of cloud usage (documents + folders + base overhead); characters/codex/timeline payloads and map images are not itemized, exactly as before — only the scope changed.

Full Changelog: v0.7.96...v0.7.98