fix: resolve white screen and black screen on daily notes

- Lazy-load WhiteboardView to prevent module-scope side effect
  (registerBuiltinCommands) from crashing the entire JS module tree
- Fix Rules of Hooks violation in Editor: hooks after early return
  caused React to crash when currentNote transitioned from null
- Fix async race condition in Editor content initialization where
  noteContent arrived after renderToDOM had already run with empty content
- Add error handling to DailyView's getOrCreateDaily call
- Add inline fallback styles to index.html for pre-JS loading state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
enzotar 2026-03-10 16:51:29 -07:00
parent 0d26e63c9a
commit 9dcfece5bf
3 changed files with 26 additions and 16 deletions

View file

@ -6,8 +6,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Graph Notes</title>
</head>
<body>
<div id="root"></div>
<body style="background:#09090b;color:#fafafa;font-family:sans-serif;">
<div id="root"><p style="padding:2rem;">Loading Graph Notes...</p></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View file

@ -1,4 +1,4 @@
import { useState, useEffect, useCallback, createContext, useContext } from "react";
import { useState, useEffect, useCallback, createContext, useContext, lazy, Suspense } from "react";
import { Routes, Route, useNavigate, useParams } from "react-router-dom";
import { Sidebar } from "./components/Sidebar";
import { Editor } from "./components/Editor";
@ -14,7 +14,7 @@ import { SearchReplace } from "./components/SearchReplace";
import { FlashcardView } from "./components/FlashcardView";
import { CSSEditor, useCustomCssInit } from "./components/CSSEditor";
import { TabBar } from "./components/TabBar";
import { WhiteboardView } from "./components/WhiteboardView";
const WhiteboardView = lazy(() => import("./components/WhiteboardView").then(m => ({ default: m.WhiteboardView })));
import { DatabaseView } from "./components/DatabaseView";
import { GitPanel } from "./components/GitPanel";
import { TimelineView } from "./components/TimelineView";
@ -385,7 +385,7 @@ export default function App() {
<Route path="/calendar" element={<CalendarView />} />
<Route path="/kanban" element={<KanbanView />} />
<Route path="/flashcards" element={<FlashcardView />} />
<Route path="/whiteboard/:name" element={<WhiteboardView />} />
<Route path="/whiteboard/:name" element={<Suspense fallback={<div className="flex-1 flex items-center justify-center"><p className="text-[var(--text-muted)]">Loading whiteboard...</p></div>}><WhiteboardView /></Suspense>} />
<Route path="/database" element={<DatabaseView />} />
<Route path="/timeline" element={<TimelineView />} />
<Route path="/analytics" element={<GraphAnalytics />} />
@ -411,6 +411,9 @@ function DailyView() {
getOrCreateDaily(vaultPath).then((dailyPath) => {
refreshNotes();
navigate(`/note/${encodeURIComponent(dailyPath)}`, { replace: true });
}).catch((e) => {
console.error("[GraphNotes] Failed to create daily note:", e);
navigate("/", { replace: true });
});
}, [vaultPath, navigate, refreshNotes]);

View file

@ -40,6 +40,7 @@ export function Editor() {
const [writingGoal, setWritingGoal] = useState(0);
const [goalEditing, setGoalEditing] = useState(false);
const lastSnapshotRef = useRef(0);
const mermaidRef = useRef<HTMLDivElement>(null);
const [noteEncrypted, setNoteEncrypted] = useState(false);
const [lockScreenOpen, setLockScreenOpen] = useState(false);
const [lockError, setLockError] = useState<string | null>(null);
@ -105,9 +106,16 @@ export function Editor() {
}, []);
// ── Initialize / switch note ──
const contentRenderedRef = useRef(false);
useEffect(() => {
if (currentNote !== lastNoteRef.current) {
lastNoteRef.current = currentNote;
contentRenderedRef.current = false;
renderToDOM(noteContent);
if (noteContent) contentRenderedRef.current = true;
} else if (!contentRenderedRef.current && noteContent) {
// Content arrived async after note switch — render it now
contentRenderedRef.current = true;
renderToDOM(noteContent);
}
}, [currentNote, noteContent, renderToDOM]);
@ -417,15 +425,7 @@ export function Editor() {
return () => document.removeEventListener("mousedown", handler);
}, []);
if (!currentNote) {
return (
<div className="flex-1 flex items-center justify-center">
<p className="text-[var(--text-muted)]">Select a note to begin editing</p>
</div>
);
}
const renderedMarkdown = (() => {
const renderedMarkdown = currentNote ? (() => {
let html = marked(noteContent, { async: false }) as string;
html = html.replace(
/\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g,
@ -436,10 +436,9 @@ export function Editor() {
}
);
return DOMPurify.sanitize(html, { ADD_ATTR: ['data-target'] });
})();
})() : "";
// Mermaid post-processing (render in a separate effect)
const mermaidRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!isPreview || !mermaidRef.current) return;
const mermaidBlocks = mermaidRef.current.querySelectorAll<HTMLElement>("code.language-mermaid");
@ -641,6 +640,14 @@ export function Editor() {
? currentNote.replace(/\.md$/, "").split("/")
: [];
if (!currentNote) {
return (
<div className="flex-1 flex items-center justify-center">
<p className="text-[var(--text-muted)]">Select a note to begin editing</p>
</div>
);
}
return (
<div className={`flex flex-col h-full ${focusMode ? "editor-focus" : ""}`} ref={editorRef}>
{/* ── Breadcrumbs ── */}