-
- {trash
- ? "Trash"
- : tag
- ? `# ${tag}`
- : query
- ? "Search results"
- : "All notes"}
-
-
- {visible.length} {visible.length === 1 ? "note" : "notes"} ·
- Edited
-
+ {error && (
+
+ {error}
+
+
+
+ )}
+ {notice && (
+
+ {notice}
+ {undo && (
+
+ )}
+ {download && (
+
+ Download {notice === "Export prepared" ? "" : "again"}
+
+ )}
+
+
+ )}
+ {importError && !setupOpen && !imports && (
+
+ {importError}
+
+ )}
+ {linkNotice && (
+
+ {linkNotice}
+
+ )}
+ {linkAdmission?.mode === "index-recovery" && (
+
+ Note links need repair. Your notes are still available.
+
+
+ )}
+ {links.error && (
+
+ {links.error}
+
+
+ )}
+ {(["study", "journal", "clips"] as const)
+ .filter(
+ (view) =>
+ (module === view ||
+ (searchOrigin?.module === view && view !== "journal")) &&
+ !(view === "journal" && (setupOpen || searchActive)),
+ )
+ .map((view) => (
+
+
+ }
+ >
+ {
+ if (canNavigate()) sourceOwners.retry();
+ }
+ : undefined
+ }
+ covered={
+ !!editing ||
+ module !== view ||
+ searchActive ||
+ setupOpen ||
+ settings ||
+ signOutOpen ||
+ !!imports ||
+ exportNotes !== null ||
+ erase ||
+ historyNote !== null ||
+ emptyConfirm !== null ||
+ incomingNoteIntentActive
+ }
+ collections={
+ sourceOwner ? sourceOwners.collections : undefined
+ }
+ onOpenSource={
+ sourceOwners.service
+ ? (request) => void openSource(request)
+ : undefined
+ }
+ openEntry={studyEntry}
+ openClip={view === "clips" ? clipSearchRequest : null}
+ holdClipSearchOrigin={searchOrigin?.module === "clips"}
+ restoreClipSearchOrigin={restoreClipSearchOrigin}
+ openCard={view === "study" ? cardSearchRequest : null}
+ onReturnToSearch={returnToSearch}
+ onSearchReaderResult={searchReaderResult}
+ view={view}
+ onEpochChange={() => {
+ selectedSnapshot.current = undefined;
+ setSelected(null);
+ }}
+ onOpenNote={(id) => {
+ if (!canNavigate()) return;
+ const linked = notes.find(
+ (note) => note.id === id && !note.trashedAt,
+ );
+ if (!linked) {
+ setNotice("This note is no longer available.");
+ return;
+ }
+ setSelected(linked.id);
+ setDrawer(false);
+ }}
+ />
+
-
- {!trash && (
- <>
-
-
+ ))}
+ {module === "study" && sourceProjectionError && (
+
+ {sourceProjectionError}
+
+ )}
+ {module === "study" && sourceUnavailable && (
+
+ {sourceUnavailable.reason === "legacy"
+ ? "This older card has no saved passage location."
+ : sourceUnavailable.reason === "stale"
+ ? "The source passage changed."
+ : sourceUnavailable.reason === "trashed"
+ ? "This source note is in Trash."
+ : "This source note is no longer available."}
+ {(sourceUnavailable.reason === "legacy" ||
+ sourceUnavailable.reason === "stale") &&
+ !editing && (
+
+ )}
+
+ )}
+ {module === "study" && editing && (
+
+ From a Study card.{" "}
+
+
+ )}
+ {module === "tags" && library && snapshot && (
+
navigate(false, name)}
+ />
+ )}
+
+ {noteActors.map((actor) => (
+
+ {
+ void openNoteTarget(target);
+ },
+ }
+ : undefined
+ }
+ store={store}
+ readOnly={readOnly}
+ sourceWriter={library?.updateWithSourceSteps}
+ sourceLibrary={library}
+ source={
+ sourceOwners.service && sourceOwners.collections && library
+ ? {
+ library,
+ collections: sourceOwners.collections,
+ service: sourceOwners.service,
+ target: actor.id === selected ? sourceTarget : null,
+ onTargetUnavailable: () => {
+ if (
+ actor.id !== selected ||
+ !sourceTarget ||
+ activeSourceTarget.current !== sourceTarget.key
+ )
+ return;
+ assignSourceTarget(null);
+ setSourceProjectionError(
+ "This saved passage target changed. Open the source again from the card.",
+ );
+ },
+ onOpenCard: openStudyEntry,
+ }
+ : undefined
+ }
+ tagCatalog={snapshot?.tags.filter(
+ (tag) => tag.state === "active",
+ )}
+ pinned={!!metadata.get(actor.id)?.pinned}
+ onPin={
+ library && snapshot
+ ? () => {
+ const token = { ...snapshot.token },
+ pinned = !metadata.get(actor.id)?.pinned;
+ libraryAction.start(
+ (id) =>
+ library.setPinned(actor.id, pinned, token, id),
+ acceptLibrary,
+ );
+ }
+ : undefined
+ }
+ onHistory={
+ library && !readOnly
+ ? () => setHistoryNote(actor)
+ : undefined
+ }
+ onSaved={(n) =>
+ setNotes((v) => [n, ...v.filter((x) => x.id !== n.id)])
+ }
+ onBack={() => {
+ setSelected(null);
+ setModule("notes");
+ }}
+ onTrash={(n) => void move(n)}
+ onExport={(n) => requestExport([n])}
+ />
+
+ ))}
+
+ {(module === "notes" || editing) && !editing && (
+ <>
+
+
+
+ {linkedOnly
+ ? "Linked"
+ : trash
+ ? "Trash"
+ : pinnedOnly
+ ? "Pinned"
+ : tag
+ ? `# ${tag}`
+ : query
+ ? "Search results"
+ : "All notes"}
+
+
+ {linkedOnly && linkedQuery?.status !== "ready"
+ ? "Checking links"
+ : `${visible.length} ${visible.length === 1 ? "note" : "notes"}`}{" "}
+ ·
+ {sort === "created"
+ ? "Created"
+ : sort === "title"
+ ? "Title"
+ : "Edited"}
+
+
+
+ {library && snapshot && (
+
+ )}
+ {trash && library && snapshot && (
-
- >
- )}
-
-
- {loaded && notes.length === 0 && (
-
-
Your notes stay in this browser
-
- This beta saves encrypted notes on this browser only. It does
- not sync or back them up. Clearing this site’s data removes
- them. Export a copy before you clear browser data or change
- devices.
-
-
- )}
- {!loaded ? (
- Loading notes…
- ) : visible.length === 0 ? (
-
-
-
- {query
- ? "No matching notes"
- : trash
- ? "Trash is empty"
- : "No notes yet"}
-
-
- {query
- ? "Try another title, word or tag."
- : trash
- ? "Notes you move to Trash appear here."
- : "Write your first note, or import Markdown files."}
-
- {query ? (
-
- ) : (
- !trash && (
+ )}
+ {!trash && (
<>
-
- ) : (
-
- {visible.map((n) => (
-
- {trash ? (
+ )}
+
+
+ {trash && (
+
+ Notes stay here until you empty Trash. Emptying Trash also
+ removes their version history and can’t be undone.
+
+ )}
+ {!loaded ? (
+ Loading notes…
+ ) : linkedOnly && linkedQuery?.status !== "ready" ? (
+
+
+ {linkedQuery?.status === "invalid"
+ ? "Note links need repair"
+ : linkedQuery?.status === "paused-limit"
+ ? "Note-link checking is paused"
+ : "Checking note links…"}
+
+
+ {linkedQuery?.status === "invalid"
+ ? "Review note-link recovery. Your notes are still available."
+ : "Complete checking to see which notes are linked."}
+
+ {linkedQuery?.status === "invalid" && (
+ setLinkRecoveryOpen(true)}>
+ Review note links
+
+ )}
+ {linkedQuery?.status !== "invalid" && links.service && (
+ {
+ if (linkVerificationPending.current) return;
+ linkVerificationPending.current = true;
+ void trackOperation(() => links.service!.verifyGraph())
+ .catch(() =>
+ setLinkNotice(
+ "Couldn’t check note links. Try again.",
+ ),
+ )
+ .finally(() => {
+ linkVerificationPending.current = false;
+ });
+ }}
+ >
+ Continue verification
+
+ )}
+ navigate()}>Show all notes
+
+ ) : visible.length === 0 ? (
+
+
+
+
+
+ {query
+ ? "No matching notes"
+ : trash
+ ? "Trash is empty"
+ : pinnedOnly
+ ? "No pinned notes yet"
+ : linkedOnly
+ ? "No linked notes yet"
+ : "No notes yet"}
+
+
+ {query
+ ? "Try another title, word or tag."
+ : trash
+ ? "Notes you move to Trash appear here."
+ : pinnedOnly
+ ? "Pin a note from its menu to see it here."
+ : linkedOnly
+ ? "Link to a note from the editor to connect your notes."
+ : "Write your first note, or bring in what you already have."}
+
+ {query ? (
+ setQuery("")}>Clear search
+ ) : (
+ !trash && (
<>
-
-
{n.title || "Untitled note"}
-
{plainText(n.document).slice(0, 160)}
+
Notes are encrypted on this device.
+
+ void create()}
+ >
+ New note
+
+ chooseMarkdown()}
+ >
+ Import notes
+
-
void restore(n)}>Restore
-
requestExport([n])}>
- Export Markdown
-
+
+ This beta doesn’t sync or back up notes. Export a copy
+ before clearing{" "}
+ {desktop ? "app data" : "this browser’s data"}.
+
>
- ) : (
-
setSelected(n.id)}
- >
- {n.title || "Untitled note"}
- {plainText(n.document).slice(0, 180)}
-
- {n.tags.map((t) => (
-
- {t}
-
- ))}
-
+ )
+ )}
+
+ ) : (
+
+ {visible.map((n) => (
+
+ {trash ? (
+ <>
+
+
{n.title || "Untitled note"}
+
{plainText(n.document).slice(0, 160)}
+
+ void restore(n)}
+ >
+ Restore
+
+ requestExport([n])}>
+ Export Markdown
+
+ >
+ ) : (
+ setSelected(n.id)}
+ >
+ {n.title || "Untitled note"}
+ {plainText(n.document).slice(0, 180)}
+
+ {metadata.get(n.id)?.pinned && (
+
+ )}
+ {n.tags.map((t) => (
+
+ {t}
+
+ ))}
+
+
+
+ )}
+ {!trash && library && snapshot && (
+
+
{
+ if (readOnlyRef.current) return;
+ const token = { ...snapshot.token },
+ pinned = !metadata.get(n.id)?.pinned;
+ libraryAction.start(
+ (id) =>
+ library.setPinned(n.id, pinned, token, id),
+ acceptLibrary,
+ );
+ }}
+ >
+
+
-
- )}
-
- ))}
-
- )}
- >
- )}
+ )}
+
+ ))}
+
+ )}
+ >
+ )}
+
void readFiles(e.target.files)}
/>
+ {linkRecoveryOpen && links.service && (
+
+ )}
+ {setupOpen && (
+ chooseMarkdown("setup")}
+ onComplete={finishSetup}
+ onClose={finishSetup}
+ />
+ )}
+ {historyNote && library && (
+ {
+ void openNoteTarget(target);
+ },
+ }
+ : undefined
+ }
+ note={notes.find((note) => note.id === historyNote.id) ?? historyNote}
+ library={library}
+ reload={reloadLibrary}
+ onClose={() => setHistoryNote(null)}
+ onResult={(result) => {
+ acceptLibrary(result);
+ if (result.note) selectedSnapshot.current = result.note;
+ }}
+ />
+ )}
+ {emptyConfirm?.mode === "ready" && library && (
+
+ )}
{settings && (
)}
@@ -655,32 +2687,53 @@ export function Workspace({
)}
{exportNotes && (
-