Something went wrong. Try again.
A local-first note taking app
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159/** * Editor layer types. Sync-backend-specific payloads live beside each editor * under `editors/<kind>/`; only that editor reads `EditorBinding.payload`. */import type { ComponentType } from 'react';
import type { DocHeading } from '../wikilinks/headings';
/** Sync/editor backend id. */export type EditorKind = 'automerge';
/** * Editor rendering mode: * - `edit` — live preview (raw syntax reveals near the cursor for editing). * - `view` — reading mode (everything stays rendered; read-only). */export type EditorMode = 'edit' | 'view';
export type EditorBinding = { kind: EditorKind; payload: unknown;};
/** * Where a tab was when you last left it. * * Switching tabs unmounts the editor (`DocumentSlotView` keys `DocumentPane` by * document id, and only the active tab's slot is rendered), so scroll position * and cursor would otherwise be lost every time. The workspace holds one of * these per tab and hands it back on the next mount. * * `scroll` is a CodeMirror scroll effect — opaque to everything but the editor * that produced it, and holding a live object, so this is in-memory only and is * deliberately never persisted. */export type EditorViewSnapshot = { selection: { anchor: number; head: number }; scroll: unknown; /** * The `reveal.seq` already applied in this tab. Returning to the tab restores * the position above rather than re-running that navigation, so a heading * jump happens once and does not replay every time you come back. */ revealedSeq?: number;};
/** Where a `[[wikilink]]` target points, and whether that document exists yet. */export type WikiLinkResolution = { /** * Provider-scoped document id (an absolute path today). For a resolved link * this is copied verbatim from the provider's listing, so it is byte-identical * to the ids the sidebar emits — which is what makes tab dedup (an exact * `providerId + entryId` string match) hit instead of opening a duplicate tab. * When `exists` is false it is the path we *would* create. */ entryId: string; /** Tab/title label — the filename, matching what the sidebar passes. */ title: string; exists: boolean;};
/** One candidate for the `[[` completion popup. */export type WikiLinkCandidate = { /** Text inserted between the brackets: the bare name, or a path if ambiguous. */ label: string; /** Secondary text — the containing folder, relative to the vault root. */ detail: string; entryId: string;};
/** * A read-only view of the vault, supplied by the app so that pure * `EditorState` decoration builders and completion sources can resolve * wikilinks *synchronously*. * * A `null` view (rather than an empty one) means "the index has not loaded * yet" — links must then render neutral rather than unresolved, so a cold open * does not flash every link as broken. */export type WikiLinkVaultView = { resolve(target: string): WikiLinkResolution | null; candidates(): readonly WikiLinkCandidate[];};
/** * Headings of notes that are *not* open, for `[[Note#` completion. * * Explicitly asynchronous and deliberately separate from `WikiLinkVaultView`, * whose synchronous contract the decoration builders depend on. Keyed by * `entryId` rather than by target text, so it duplicates none of the resolution * logic in `wikiLinkIndex` — the caller resolves first, then asks here. */export type WikiLinkHeadingSource = { load(entryId: string): Promise<readonly DocHeading[]>;};
/** A wikilink the user activated, already resolved against the vault. */export type WikiLinkActivation = WikiLinkResolution & { /** `#heading` text without the `#`, or null. Carried but not yet acted on. */ anchor: string | null; /** The target text as typed, for diagnostics. */ rawTarget: string;};
/** Props passed from workspaces into every registered editor component. */export type EditorProps = { binding: EditorBinding; className?: string; placeholder?: string; /** Rendering mode for this view; defaults to `edit`. */ mode?: EditorMode; /** * Called when a link to an internal (schemeless) target is activated — the raw * link string, for the workspace to resolve against the current document and * open. External links (with a URI scheme) are opened by the editor itself. */ onOpenLink?: (target: string) => void; /** * Vault knowledge for `[[wikilinks]]`: resolution (for rendering resolved vs * unresolved) and the candidate list (for completion). `null` while the * vault index is still loading. */ wikiLinkVault?: WikiLinkVaultView | null; /** Headings of other notes, for `[[Note#` completion. Null while unavailable. */ wikiLinkHeadings?: WikiLinkHeadingSource | null; /** * Called when a wikilink is activated, already resolved. Unlike `onOpenLink` * the editor cannot resolve this itself, so the app both resolves it (via * `wikiLinkVault`) and handles creating the note when it does not exist. */ onOpenWikiLink?: (activation: WikiLinkActivation) => void; /** * Position to restore on mount — where this tab was when it was last shown. * Absent for a document being opened for the first time. */ viewSnapshot?: EditorViewSnapshot | null; /** * Called as the editor tears down, with the position to restore next time. * The workspace stores it against the tab being left. */ onViewSnapshot?: (snapshot: EditorViewSnapshot) => void; /** * Scroll to and place the caret at a heading. `seq` is monotonic, so * requesting the *same* anchor again still fires; the editor ignores a seq it * has already applied, which is what stops a stale intent replaying when you * return to the tab. */ reveal?: { seq: number; anchor: string } | null;};
/** * A registered editor: UI for one sync method, keyed by `kind`. * Implementations live under `editors/<kind>/` and are listed in `registry.ts`. */export type Editor = { readonly kind: EditorKind; readonly Component: ComponentType<EditorProps>;};