From 385c6a8a7b1404b776a97406c6498c1d39ffffdc Mon Sep 17 00:00:00 2001 From: Adam Martin Date: Tue, 11 Aug 2026 21:42:16 -0500 Subject: [PATCH 1/2] feat: Move pane sync state into provider --- apps/web/src/components/note-editor.tsx | 181 +-------------- packages/editor/src/native-source-editor.tsx | 6 +- packages/editor/src/pane-sync-context.tsx | 221 +++++++++++++++++-- packages/editor/src/rendered-editor.tsx | 3 +- 4 files changed, 221 insertions(+), 190 deletions(-) diff --git a/apps/web/src/components/note-editor.tsx b/apps/web/src/components/note-editor.tsx index ad107d8..d7eb46c 100644 --- a/apps/web/src/components/note-editor.tsx +++ b/apps/web/src/components/note-editor.tsx @@ -4,18 +4,13 @@ import { EditorToolbar, type EditorMode, type MarkdownViewState, - type MarkdownPane, MARKDOWN_NOTE_TYPE_ID, NativeSourceEditor, NoteToolbar, PaneSyncProvider, - type PaneSyncContextValue, getNoteContextMenuBuilder, getNoteViewComponent, type RendererProps, - useCursorSync, - useFocusSync, - useScrollSync, } from "@notes/editor"; import { CANVAS_NOTE_TYPE_ID } from "@notes/note-canvas"; import type { FileTypeHandler } from "@notes/plugin-host"; @@ -40,7 +35,6 @@ import { connectTomeChanges } from "../api/ws"; import { frontmatterType } from "../lib/frontmatter"; import { isImagePath } from "../lib/images"; import { useAppServices } from "../state/app-services"; -import { useEditorCallbacks } from "../state/use-editor-callbacks"; import { useToasts } from "../state/toast"; import { ModeToggle, SaveState } from "./mode-toggle"; @@ -127,7 +121,6 @@ export function NoteEditor({ }) { const { markModified, setActiveDocument, fileHandlers, noteTypeRegistry } = useAppServices(); const { notify } = useToasts(); - const callbacks = useEditorCallbacks(isStandalone); const stateKey = editorStateKey(path); const initialSession = markdownSessionByPath.get(stateKey) ?? { mode: defaultMode, @@ -141,8 +134,6 @@ export function NoteEditor({ const [saveState, setSaveState] = useState("loading"); const [splitScrollSync, setSplitScrollSync] = useState(initialSession.viewState.splitScrollSync); const ctxMenu = useContextMenu(); - // Component-registered context menu builder (e.g. board view registers Delete/Duplicate). - // Takes priority over the descriptor-declared builder below. const [componentCtxBuilder, setNoteViewCtxBuilder] = useState( null, ); @@ -153,22 +144,6 @@ export function NoteEditor({ const lastWriteAtRef = useRef(0); const isImage = isImagePath(path); - // ── Per-pane sync hooks (cursor, scroll, focus) ─────────────────────────── - const sourceCursor = useCursorSync(initialSession.viewState.sourceCursor); - const renderedCursor = useCursorSync(initialSession.viewState.renderedCursor); - const sourceScroll = useScrollSync(initialSession.viewState.sourceScrollRatio); - const renderedScroll = useScrollSync(initialSession.viewState.renderedScrollRatio); - const sourceFocus = useFocusSync(); - const renderedFocus = useFocusSync(); - - const activePaneRef = useRef(initialSession.viewState.lastFocusedPane); - const sourceCursorPosRef = useRef(initialSession.viewState.sourceCursor); - const renderedCursorPosRef = useRef(initialSession.viewState.renderedCursor); - const prevModeRef = useRef(mode); - // Scroll-sync lock refs — prevent feedback loops when programmatically scrolling. - const sourceScrollLock = useRef(false); - const renderedScrollLock = useRef(false); - const persistSession = useCallback( (nextMode: EditorMode, nextViewState: MarkdownViewState) => { markdownSessionByPath.set(stateKey, { mode: nextMode, viewState: nextViewState }); @@ -176,126 +151,6 @@ export function NoteEditor({ [stateKey], ); - useEffect(() => { - persistSession(mode, markdownViewState); - }, [mode, markdownViewState, persistSession]); - - // ── Pane sync logic ─────────────────────────────────────────────────────── - - const preferredPaneForMode = useCallback((nextMode: EditorMode): MarkdownPane => { - if (nextMode === "edit") return "source"; - if (nextMode === "rendered") return "rendered"; - return activePaneRef.current; - }, []); - - const { send: sendSourceFocus } = sourceFocus; - const { send: sendRenderedFocus } = renderedFocus; - const requestPaneFocus = useCallback( - (pane: MarkdownPane) => { - if (pane === "source") sendSourceFocus(); - else sendRenderedFocus(); - }, - [sendSourceFocus, sendRenderedFocus], - ); - - useEffect(() => { - requestPaneFocus(preferredPaneForMode(mode)); - }, [mode, preferredPaneForMode, requestPaneFocus]); - - const { send: sendSourceCursor } = sourceCursor; - const { send: sendRenderedCursor } = renderedCursor; - useEffect(() => { - const prev = prevModeRef.current; - if (prev === mode) return; - - if (mode === "edit") { - const position = - activePaneRef.current === "rendered" - ? renderedCursorPosRef.current - : sourceCursorPosRef.current; - sendSourceCursor(position); - requestPaneFocus("source"); - } else if (mode === "rendered") { - const position = - activePaneRef.current === "source" - ? sourceCursorPosRef.current - : renderedCursorPosRef.current; - sendRenderedCursor(position); - requestPaneFocus("rendered"); - } else { - requestPaneFocus(preferredPaneForMode(mode)); - } - - prevModeRef.current = mode; - }, [mode, preferredPaneForMode, requestPaneFocus, sendSourceCursor, sendRenderedCursor]); - - const emitViewState = useCallback( - (patch: Partial) => { - setMarkdownViewState((prev) => { - const next = { ...prev, ...patch }; - persistSession(mode, next); - return next; - }); - }, - [persistSession, mode], - ); - - const handleSourceFocus = useCallback(() => { - activePaneRef.current = "source"; - emitViewState({ lastFocusedPane: "source" }); - }, [emitViewState]); - - const handleRenderedFocus = useCallback(() => { - activePaneRef.current = "rendered"; - emitViewState({ lastFocusedPane: "rendered" }); - }, [emitViewState]); - - const handleSourceCursorChange = useCallback( - (position: number) => { - sourceCursorPosRef.current = position; - emitViewState({ sourceCursor: position }); - }, - [emitViewState], - ); - - const handleRenderedCursorChange = useCallback( - (position: number) => { - renderedCursorPosRef.current = position; - emitViewState({ renderedCursor: position }); - }, - [emitViewState], - ); - - const { send: sendRenderedScroll } = renderedScroll; - const handleSourceScrollChange = useCallback( - (ratio: number) => { - emitViewState({ sourceScrollRatio: ratio }); - if (!splitScrollSync || mode !== "split") return; - if (sourceScrollLock.current) { - sourceScrollLock.current = false; - return; - } - renderedScrollLock.current = true; - sendRenderedScroll(ratio); - }, - [emitViewState, mode, splitScrollSync, sendRenderedScroll], - ); - - const { send: sendSourceScroll } = sourceScroll; - const handleRenderedScrollChange = useCallback( - (ratio: number) => { - emitViewState({ renderedScrollRatio: ratio }); - if (!splitScrollSync || mode !== "split") return; - if (renderedScrollLock.current) { - renderedScrollLock.current = false; - return; - } - sourceScrollLock.current = true; - sendSourceScroll(ratio); - }, - [emitViewState, mode, splitScrollSync, sendSourceScroll], - ); - // Flush any unsaved edit when the component unmounts (tab switch / close). useEffect(() => { return () => { @@ -439,32 +294,6 @@ export function NoteEditor({ const showSource = effectiveMode === "edit" || effectiveMode === "split"; const showRendered = effectiveMode === "rendered" || effectiveMode === "split"; - // Build the pane-sync context value for the markdown editor panes. - const paneSyncValue: PaneSyncContextValue = { - source: { - cursorRequest: sourceCursor.request, - scrollRequest: descriptorSupportsScrollSync ? sourceScroll.request : undefined, - focusRequest: sourceFocus.request, - onCursorChange: handleSourceCursorChange, - onScrollChange: descriptorSupportsScrollSync ? handleSourceScrollChange : undefined, - onFocus: handleSourceFocus, - isReadOnly: descriptorSourceProtected, - }, - rendered: { - cursorRequest: renderedCursor.request, - scrollRequest: descriptorSupportsScrollSync ? renderedScroll.request : undefined, - focusRequest: renderedFocus.request, - onCursorChange: handleRenderedCursorChange, - onScrollChange: descriptorSupportsScrollSync ? handleRenderedScrollChange : undefined, - onFocus: handleRenderedFocus, - // Adapt state-setter to the context's plain callback signature. - onRegisterContextMenu: (builder) => - setNoteViewCtxBuilder(builder as NoteViewContextMenuBuilder | null), - }, - callbacks, - isStandalone, - }; - // Publish the active document to plugins (word count, etc.). useEffect(() => { if (saveState === "loading") { @@ -595,7 +424,15 @@ export function NoteEditor({ ) : ( - +
{effectiveMode === "edit" && }
diff --git a/packages/editor/src/native-source-editor.tsx b/packages/editor/src/native-source-editor.tsx index 85a1278..e8ad07b 100644 --- a/packages/editor/src/native-source-editor.tsx +++ b/packages/editor/src/native-source-editor.tsx @@ -1,4 +1,5 @@ -import { +import { useEditorCallbacks } from "@notes/web/src/state/use-editor-callbacks"; +import { ClipboardEventHandler, DragEventHandler, SyntheticEvent, @@ -12,7 +13,6 @@ import { useSourcePaneSync } from "./pane-sync-context"; export function NativeSourceEditor({ value, onChange }: RendererProps) { const { - callbacks, scrollRequest, onScrollChange, focusRequest, @@ -20,7 +20,9 @@ export function NativeSourceEditor({ value, onChange }: RendererProps) { cursorRequest, onCursorChange, isReadOnly, + isStandalone = false, } = useSourcePaneSync() ?? {}; + const callbacks = useEditorCallbacks(isStandalone); const effectiveOnChange = isReadOnly ? () => {} : onChange; const viewRef = useRef(null); diff --git a/packages/editor/src/pane-sync-context.tsx b/packages/editor/src/pane-sync-context.tsx index f3f47d5..15b110e 100644 --- a/packages/editor/src/pane-sync-context.tsx +++ b/packages/editor/src/pane-sync-context.tsx @@ -9,8 +9,24 @@ * PaneSyncProvider, which lets RenderedEditor work standalone (board cards, * calendar modals) without sync or callbacks. */ -import { createContext, type ReactNode, useContext } from "react"; -import type { CursorRequest, EditorCallbacks, FocusRequest, ScrollRequest } from "./types"; +import { + createContext, + type ReactNode, + useCallback, + useContext, + useEffect, + useMemo, + useRef, +} from "react"; +import { useCursorSync, useFocusSync, useScrollSync } from "./use-pane-sync"; +import type { + CursorRequest, + EditorMode, + FocusRequest, + MarkdownPane, + MarkdownViewState, + ScrollRequest, +} from "./types"; /** State exposed to the source (raw-text) pane. */ export interface SourcePaneState { @@ -32,29 +48,204 @@ export interface RenderedPaneState { onCursorChange: (position: number) => void; onScrollChange: ((ratio: number) => void) | undefined; onFocus: () => void; - /** - * Called by the rendered pane to register / unregister a note-type-specific - * context-menu builder. Pass null to unregister. - */ - onRegisterContextMenu: (builder: ((target: Element | null) => unknown[] | null) | null) => void; } -export interface PaneSyncContextValue { +interface PaneSyncContextValue { source: SourcePaneState; rendered: RenderedPaneState; - callbacks: EditorCallbacks; isStandalone: boolean; } const PaneSyncContext = createContext(null); export function PaneSyncProvider({ - value, children, + setMarkdownViewState, + persistSession, + mode, + initialSession, + splitScrollSync, + isReadOnly, + isStandalone, }: { - value: PaneSyncContextValue; children: ReactNode; + setMarkdownViewState: React.Dispatch>; + persistSession: (mode: EditorMode, state: MarkdownViewState) => void; + mode: EditorMode; + initialSession: { + mode: EditorMode; + viewState: MarkdownViewState; + }; + splitScrollSync: boolean; + isReadOnly: boolean; + isStandalone: boolean; }) { + const { send: sendSourceCursor, request: requestSourceCursor } = useCursorSync( + initialSession.viewState.sourceCursor, + ); + const { send: sendRenderedCursor, request: requestRenderedCursor } = useCursorSync( + initialSession.viewState.renderedCursor, + ); + const { send: sendSourceScroll, request: requestSourceScroll } = useScrollSync( + initialSession.viewState.sourceScrollRatio, + ); + const { send: sendRenderedScroll, request: requestRenderedScroll } = useScrollSync( + initialSession.viewState.renderedScrollRatio, + ); + const { send: sendSourceFocus, request: requestSourceFocus } = useFocusSync(); + const { send: sendRenderedFocus, request: requestRenderedFocus } = useFocusSync(); + + const activePaneRef = useRef(initialSession.viewState.lastFocusedPane); + const sourceCursorPosRef = useRef(initialSession.viewState.sourceCursor); + const renderedCursorPosRef = useRef(initialSession.viewState.renderedCursor); + const prevModeRef = useRef(mode); + + // Scroll-sync lock refs — prevent feedback loops when programmatically scrolling. + const sourceScrollLock = useRef(false); + const renderedScrollLock = useRef(false); + + const preferredPaneForMode = useCallback((nextMode: EditorMode): MarkdownPane => { + if (nextMode === "edit") return "source"; + if (nextMode === "rendered") return "rendered"; + return activePaneRef.current; + }, []); + + const requestPaneFocus = useCallback( + (pane: MarkdownPane) => { + if (pane === "source") sendSourceFocus(); + else sendRenderedFocus(); + }, + [sendSourceFocus, sendRenderedFocus], + ); + + useEffect(() => { + const prev = prevModeRef.current; + if (prev === mode) return; + + if (mode === "edit") { + const position = + activePaneRef.current === "rendered" + ? renderedCursorPosRef.current + : sourceCursorPosRef.current; + sendSourceCursor(position); + requestPaneFocus("source"); + } else if (mode === "rendered") { + const position = + activePaneRef.current === "source" + ? sourceCursorPosRef.current + : renderedCursorPosRef.current; + sendRenderedCursor(position); + requestPaneFocus("rendered"); + } else { + requestPaneFocus(preferredPaneForMode(mode)); + } + + prevModeRef.current = mode; + }, [mode, preferredPaneForMode, requestPaneFocus, sendSourceCursor, sendRenderedCursor]); + + const emitViewState = useCallback( + (patch: Partial) => { + setMarkdownViewState((prev) => { + const next = { ...prev, ...patch }; + persistSession(mode, next); + return next; + }); + }, + [setMarkdownViewState, persistSession, mode], + ); + + const handleSourceFocus = useCallback(() => { + activePaneRef.current = "source"; + emitViewState({ lastFocusedPane: "source" }); + }, [emitViewState]); + + const handleRenderedFocus = useCallback(() => { + activePaneRef.current = "rendered"; + emitViewState({ lastFocusedPane: "rendered" }); + }, [emitViewState]); + + const handleSourceCursorChange = useCallback( + (position: number) => { + sourceCursorPosRef.current = position; + emitViewState({ sourceCursor: position }); + }, + [emitViewState], + ); + + const handleRenderedCursorChange = useCallback( + (position: number) => { + renderedCursorPosRef.current = position; + emitViewState({ renderedCursor: position }); + }, + [emitViewState], + ); + + const handleSourceScrollChange = useCallback( + (ratio: number) => { + emitViewState({ sourceScrollRatio: ratio }); + if (!splitScrollSync || mode !== "split") return; + if (sourceScrollLock.current) { + sourceScrollLock.current = false; + return; + } + renderedScrollLock.current = true; + sendRenderedScroll(ratio); + }, + [emitViewState, mode, splitScrollSync, sendRenderedScroll], + ); + + const handleRenderedScrollChange = useCallback( + (ratio: number) => { + emitViewState({ renderedScrollRatio: ratio }); + if (!splitScrollSync || mode !== "split") return; + if (renderedScrollLock.current) { + renderedScrollLock.current = false; + return; + } + sourceScrollLock.current = true; + sendSourceScroll(ratio); + }, + [emitViewState, mode, splitScrollSync, sendSourceScroll], + ); + + const value: PaneSyncContextValue = useMemo( + () => ({ + source: { + cursorRequest: requestSourceCursor, + scrollRequest: requestSourceScroll, + focusRequest: requestSourceFocus, + onCursorChange: handleSourceCursorChange, + onScrollChange: handleSourceScrollChange, + onFocus: handleSourceFocus, + isReadOnly, + }, + rendered: { + cursorRequest: requestRenderedCursor, + scrollRequest: requestRenderedScroll, + focusRequest: requestRenderedFocus, + onCursorChange: handleRenderedCursorChange, + onScrollChange: handleRenderedScrollChange, + onFocus: handleRenderedFocus, + }, + isStandalone, + }), + [ + handleRenderedCursorChange, + handleRenderedFocus, + handleRenderedScrollChange, + handleSourceCursorChange, + handleSourceFocus, + handleSourceScrollChange, + isReadOnly, + isStandalone, + requestRenderedCursor, + requestRenderedFocus, + requestRenderedScroll, + requestSourceCursor, + requestSourceFocus, + requestSourceScroll, + ], + ); return {children}; } @@ -63,10 +254,10 @@ export function PaneSyncProvider({ * Returns undefined when called outside a provider (standalone use). */ export function useSourcePaneSync(): - (SourcePaneState & Pick) | undefined { + (SourcePaneState & Pick) | undefined { const ctx = useContext(PaneSyncContext); if (!ctx) return undefined; - return { ...ctx.source, callbacks: ctx.callbacks, isStandalone: ctx.isStandalone }; + return { ...ctx.source, isStandalone: ctx.isStandalone }; } /** @@ -74,8 +265,8 @@ export function useSourcePaneSync(): * Returns undefined when called outside a provider (standalone use). */ export function useRenderedPaneSync(): - (RenderedPaneState & Pick) | undefined { + (RenderedPaneState & Pick) | undefined { const ctx = useContext(PaneSyncContext); if (!ctx) return undefined; - return { ...ctx.rendered, callbacks: ctx.callbacks, isStandalone: ctx.isStandalone }; + return { ...ctx.rendered, isStandalone: ctx.isStandalone }; } diff --git a/packages/editor/src/rendered-editor.tsx b/packages/editor/src/rendered-editor.tsx index 317075d..a1cbb76 100644 --- a/packages/editor/src/rendered-editor.tsx +++ b/packages/editor/src/rendered-editor.tsx @@ -1,6 +1,7 @@ import { FindBar } from "@notes/web/src/components/find-bar"; import { buildContent, parseFrontmatter } from "@notes/web/src/lib/frontmatter"; import { useAppServices } from "@notes/web/src/state/app-services"; +import { useEditorCallbacks } from "@notes/web/src/state/use-editor-callbacks"; import TaskItem from "@tiptap/extension-task-item"; import TaskList from "@tiptap/extension-task-list"; import { Table } from "@tiptap/extension-table"; @@ -89,7 +90,6 @@ export function RenderedEditor({ }: Omit & { toolbarDisabled?: boolean; path?: string }) { // Context wins over props; props are fallbacks for standalone usage. const { - callbacks, isStandalone = false, cursorRequest, scrollRequest, @@ -98,6 +98,7 @@ export function RenderedEditor({ onFocus, focusRequest, } = useRenderedPaneSync() ?? {}; + const callbacks = useEditorCallbacks(isStandalone); const { settings } = useAppServices(); const [findOpen, setFindOpen] = useState(false); const currentParts = parseFrontmatter(value); From ff0496137a823b440a6f4c56d09de8ff3ebcb16d Mon Sep 17 00:00:00 2001 From: Adam Martin Date: Tue, 11 Aug 2026 22:14:41 -0500 Subject: [PATCH 2/2] fix: Fix customer context menu registration --- apps/web/src/components/note-editor.tsx | 25 ++++++++++++--------- packages/editor/src/note-view-descriptor.ts | 9 +++----- packages/editor/src/types.ts | 6 ++--- packages/note-boards/src/board-view.tsx | 4 ++-- packages/ui/src/context-menu.tsx | 2 +- packages/ui/src/index.ts | 6 +---- 6 files changed, 24 insertions(+), 28 deletions(-) diff --git a/apps/web/src/components/note-editor.tsx b/apps/web/src/components/note-editor.tsx index d7eb46c..58d879d 100644 --- a/apps/web/src/components/note-editor.tsx +++ b/apps/web/src/components/note-editor.tsx @@ -17,7 +17,7 @@ import type { FileTypeHandler } from "@notes/plugin-host"; import { ContextMenu, ContextMenuEntry, - type NoteViewContextMenuBuilder, + type CustomContextMenuBuilder, useContextMenu, } from "@notes/ui"; import { @@ -134,9 +134,7 @@ export function NoteEditor({ const [saveState, setSaveState] = useState("loading"); const [splitScrollSync, setSplitScrollSync] = useState(initialSession.viewState.splitScrollSync); const ctxMenu = useContextMenu(); - const [componentCtxBuilder, setNoteViewCtxBuilder] = useState( - null, - ); + const [customCtxBuilder, setCustomCtxBuilder] = useState(null); const regionRef = useRef(null); const dirtyRef = useRef(false); const contentRef = useRef(""); @@ -151,6 +149,11 @@ export function NoteEditor({ [stateKey], ); + const registerCustomCtxBuilder = useCallback((builder: CustomContextMenuBuilder | null) => { + // setState dispatch is a function, so to store a function, we need to use the function syntax + setCustomCtxBuilder(() => builder); + }, []); + // Flush any unsaved edit when the component unmounts (tab switch / close). useEffect(() => { return () => { @@ -316,7 +319,7 @@ export function NoteEditor({ const descriptorCtxBuilder = activeDescriptor ? (getNoteContextMenuBuilder(activeDescriptor) ?? null) : null; - const noteViewCtxBuilder = componentCtxBuilder ?? descriptorCtxBuilder; + const ctxBuilder = customCtxBuilder ?? descriptorCtxBuilder; const runEditCommand = useCallback( (command: string, target: HTMLElement | null) => { @@ -330,8 +333,8 @@ export function NoteEditor({ const contextMenuItems = useMemo(() => { const target = ctxMenu.menu?.data ?? null; - if (typeof noteViewCtxBuilder === "function") { - const custom = noteViewCtxBuilder(target); + if (ctxBuilder) { + const custom = ctxBuilder(target); if (custom) return custom; } @@ -347,7 +350,7 @@ export function NoteEditor({ { label: "Select All", run: () => runEditCommand("selectAll", target) }, ); return items; - }, [ctxMenu.menu?.data, noteViewCtxBuilder, runEditCommand]); + }, [ctxMenu.menu?.data, ctxBuilder, runEditCommand]); if (saveState === "loading") { return
Loading…
; @@ -363,8 +366,8 @@ export function NoteEditor({ if (!target || target.closest(".context-menu")) { return; } - if (typeof noteViewCtxBuilder === "function") { - const custom = noteViewCtxBuilder(target); + if (ctxBuilder) { + const custom = ctxBuilder(target); // If the note view's builder returns [] (empty), suppress the menu entirely. if (custom !== undefined && custom !== null && custom.length === 0) return; } @@ -447,7 +450,7 @@ export function NoteEditor({ path={path} value={content} onChange={handleChange} - onRegisterContextMenu={setNoteViewCtxBuilder} + onRegisterContextMenu={registerCustomCtxBuilder} /> )} {showRendered && !noteRenderer && ( diff --git a/packages/editor/src/note-view-descriptor.ts b/packages/editor/src/note-view-descriptor.ts index ef21971..296f697 100644 --- a/packages/editor/src/note-view-descriptor.ts +++ b/packages/editor/src/note-view-descriptor.ts @@ -11,7 +11,7 @@ */ import type { NoteTypeDetector } from "@notes/core"; import type { ComponentType, ReactNode } from "react"; -import type { NoteViewContextMenuBuilder, ContextMenuEntry } from "@notes/ui"; +import type { CustomContextMenuBuilder } from "@notes/ui"; import type { EditorMode, RendererProps } from "./types"; export type NoteViewComponent = ComponentType; @@ -28,9 +28,6 @@ export interface NoteTypeToolbarItem { element: ReactNode; } -// Re-export for convenience. -export type { NoteViewContextMenuBuilder, ContextMenuEntry }; - /** * Complete descriptor for a note type (id, detect, and all view capabilities). * @@ -53,7 +50,7 @@ export interface NoteTypeDescriptor extends NoteTypeDetector { /** Toolbar items contributed by this note type. */ toolbarItems?: NoteTypeToolbarItem[]; /** Builds note-type-specific context menu items on right-click. */ - contextMenuBuilder?: NoteViewContextMenuBuilder; + contextMenuBuilder?: CustomContextMenuBuilder; } // ── Accessor helpers ────────────────────────────────────────────────────────── @@ -68,6 +65,6 @@ export function getNoteViewToolbarItems(d: NoteTypeDescriptor): NoteTypeToolbarI export function getNoteContextMenuBuilder( d: NoteTypeDescriptor, -): NoteViewContextMenuBuilder | undefined { +): CustomContextMenuBuilder | undefined { return d.contextMenuBuilder; } diff --git a/packages/editor/src/types.ts b/packages/editor/src/types.ts index 3ae4be7..1e94bed 100644 --- a/packages/editor/src/types.ts +++ b/packages/editor/src/types.ts @@ -1,5 +1,5 @@ -import type { NoteViewContextMenuBuilder } from "@notes/ui"; -import type { Dispatch, ReactNode, SetStateAction } from "react"; +import type { CustomContextMenuBuilder } from "@notes/ui"; +import type { ReactNode } from "react"; /** The three editor modes available for a note-type view. */ export type EditorMode = "edit" | "split" | "rendered"; @@ -128,5 +128,5 @@ export interface RendererProps { * Pass null (or call with null at cleanup) to unregister. * Typed as unknown[] to avoid importing @notes/ui here; consumers cast to ContextMenuEntry[]. */ - onRegisterContextMenu?: Dispatch>; + onRegisterContextMenu?: (builder: CustomContextMenuBuilder | null) => void; } diff --git a/packages/note-boards/src/board-view.tsx b/packages/note-boards/src/board-view.tsx index 99c45d2..d327715 100644 --- a/packages/note-boards/src/board-view.tsx +++ b/packages/note-boards/src/board-view.tsx @@ -1,7 +1,7 @@ import { Fragment, useCallback, useEffect, useMemo, useRef, useState, type DragEvent } from "react"; import { usePromptDialog, type RendererProps } from "@notes/editor"; import { debounce } from "@notes/core"; -import { type NoteViewContextMenuBuilder } from "@notes/ui"; +import { type CustomContextMenuBuilder } from "@notes/ui"; import { useUndoStack } from "@notes/web/src/state/undo-context"; import { BoardColumn } from "./board-column"; import { BoardCardModal } from "./board-card-modal"; @@ -328,7 +328,7 @@ export function BoardView({ value, onChange, path, onRegisterContextMenu }: Rend // a card shows card-specific actions instead of the generic edit menu. useEffect(() => { if (!onRegisterContextMenu) return; - const builder: NoteViewContextMenuBuilder = (target) => { + const builder: CustomContextMenuBuilder = (target) => { const cardEl = target?.closest("[data-card-id]"); // Return [] (not null) so the generic edit menu (undo/redo/cut…) is // suppressed entirely — those commands don't apply to board operations. diff --git a/packages/ui/src/context-menu.tsx b/packages/ui/src/context-menu.tsx index 78e2100..be86566 100644 --- a/packages/ui/src/context-menu.tsx +++ b/packages/ui/src/context-menu.tsx @@ -37,7 +37,7 @@ export type ContextMenuEntry = ContextMenuItemDef | ContextMenuSeparatorDef; * }, [onRegisterContextMenu]); * ``` */ -export type NoteViewContextMenuBuilder = (target: Element | null) => ContextMenuEntry[] | null; +export type CustomContextMenuBuilder = (target: Element | null) => ContextMenuEntry[] | null; interface ContextMenuProps { position: MenuPosition; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 6a260c8..f1ac3b0 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -4,11 +4,7 @@ export const PACKAGE_NAME = "@notes/ui"; export { PopupMenu } from "./popup-menu"; export { Island, IslandHeader, IslandBody, IslandFooter } from "./island"; export { TabStrip, Tab } from "./tabs"; -export { - ContextMenu, - type ContextMenuEntry, - type NoteViewContextMenuBuilder, -} from "./context-menu"; +export { ContextMenu, type ContextMenuEntry, type CustomContextMenuBuilder } from "./context-menu"; export { PanelGroup, PanelSection, PanelHeader, PanelBody, PanelEmpty } from "./panel"; export { Modal, ModalHeader, ModalBody, ModalFooter } from "./modal";