/** A shared Markdown document with Fabric profile names on live cursors. */ import { computed, Default, handler, NAME, pattern, type PerSpace, type PerUser, TILE_UI, UI, type VNode, wish, Writable, } from "commonfabric"; import { normalizePresenceParticipantName } from "./participant-name.ts"; /** Initial document values, shared by every viewer of this piece. */ export interface SharedNoteInput { /** Editable document title, independent of Markdown frontmatter. */ title?: PerSpace>; /** LF-normalized Markdown; live editing uses CodeMirror operations. */ content?: PerSpace>; } /** Readable Markdown and the same compact view for standalone and embedded use. */ export interface SharedNoteOutput { [NAME]: string; [UI]: VNode; [TILE_UI]: VNode; title: PerSpace; content: PerSpace; participantName: PerUser; } const reportError = handler< { detail: { message?: string } }, { error: Writable } >((event, { error }) => { error.set( event.detail.message || "Editing paused. Reopen this note to reconnect.", ); }); const preserveUnsentText = handler< { detail: { localValue: string } }, { recovery: Writable } >((event, { recovery }) => { recovery.set(event.detail.localValue); }); // A presence failure removes live cursors and leaves editing alone. The editor // tries a `configuration` failure again only when the room or name changes, // and any other failure when it next gains focus. A failure while it has focus // keeps focus, so the viewer has to leave the note and come back. const reportPresenceError = handler< { detail: { category?: string } }, { presenceNotice: Writable } >((event, { presenceNotice }) => { presenceNotice.set( event.detail.category === "configuration" ? "Live cursors are unavailable on this server. Editing still works." : "Live cursors are unavailable. Editing still works; click outside the note, then back into it to try again.", ); }); const clearPresenceNotice = handler< unknown, { presenceNotice: Writable } >((_, { presenceNotice }) => { presenceNotice.set(""); }); export default pattern( ({ title, content }) => { const profile = wish<{ name?: string; avatar?: string }>({ query: "#profile", }); const profileName = wish({ query: "#profileName" }); const participantName = computed(() => normalizePresenceParticipantName(profileName.result ?? "") ); const hasProfile = computed(() => participantName !== "" && profile.result !== undefined ); const error = new Writable.perSession(""); const recovery = new Writable.perSession(null); const hasError = computed(() => error.get() !== ""); const presenceNotice = new Writable.perSession(""); const hasPresenceNotice = computed(() => presenceNotice.get() !== ""); const hasRecovery = computed(() => recovery.get() !== null); const recoveryText = computed(() => recovery.get() ?? ""); const view = ( {hasProfile ? : (
Choose a Fabric profile to label your cursor. {profile[UI]}
)}
{hasError ? {error} : null} {hasPresenceNotice ? {presenceNotice} : null} {hasRecovery ? ( Editing paused. Copy your unsent text below before reopening this note. ) : null}
); return { [NAME]: title, [UI]: view, [TILE_UI]: view, title, content, participantName, }; }, );