@meowdown/react 0.23.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,6 +2,28 @@
2
2
 
3
3
  React components for Meowdown, a hybrid (live-preview) Markdown editor.
4
4
 
5
+ [**Live demo**](https://meowdown.vercel.app/)
6
+
7
+ ## Quick start
8
+
9
+ Install the package and its peer dependencies:
10
+
11
+ ```sh
12
+ npm install @meowdown/react @meowdown/core react react-dom
13
+ ```
14
+
15
+ Import both stylesheets and render the editor:
16
+
17
+ ```tsx
18
+ import '@meowdown/core/style.css'
19
+ import '@meowdown/react/style.css'
20
+ import { MeowdownEditor } from '@meowdown/react'
21
+
22
+ export function App() {
23
+ return <MeowdownEditor initialMarkdown="# Hello" />
24
+ }
25
+ ```
26
+
5
27
  ## Usage
6
28
 
7
29
  ```tsx
@@ -30,85 +52,7 @@ export function App() {
30
52
 
31
53
  ## API
32
54
 
33
- ### `<MeowdownEditor>`
34
-
35
- The Markdown editor component. Renders inside a `div.meowdown` wrapper that fills a flex parent. In rich modes, typing `/` opens a slash menu for inserting blocks (headings, blockquote, lists, code block, table). Hovering a block shows a handle to its left: the grip selects the block and can be dragged to move it, with a drop indicator line marking the target.
36
-
37
- - `mode?: 'focus' | 'show' | 'hide' | 'source'`: defaults to `'focus'`.
38
- - `'focus'`: Markdown syntax is hidden, revealed around the cursor.
39
- - `'show'`: Markdown syntax is always visible.
40
- - `'hide'`: Markdown syntax is always hidden.
41
- - `'source'`: raw Markdown source with syntax highlighting.
42
- - `initialMarkdown?: string`: first render only.
43
- - `onDocChange?: VoidFunction`: called on every user-driven document change. Programmatic `setMarkdown` / `setState` on the handle do not fire it.
44
- - `onTagSearch?: (query: string) => TagItem[] | Promise<TagItem[]>`: enables the tag menu, which opens when typing `#` followed by text in a rich mode. Returns ranked rows `{ tag, label?, detail?, onSelect? }` (the menu does not re-sort). Selecting a row inserts `#tag ` then runs its `onSelect`. Omit to disable.
45
- - `onWikilinkSearch?: (query: string) => WikilinkItem[] | Promise<WikilinkItem[]>`: enables the wikilink menu, which opens as soon as `[[` or `@` is typed, or `Mod-Shift-k` is pressed, in a rich mode; the shortcut seeds the query from any selected text. Returns ranked rows `{ target, label?, detail?, onSelect? }` (the menu does not re-sort). Selecting a row inserts `[[target]]` then runs its `onSelect`. Omit to disable.
46
- - `onWikilinkClick?: (payload: { target: string; event: MouseEvent }) => void`: called when a rendered wiki link is clicked. Pass a stable function (e.g. from `useCallback`). Ignored in source mode.
47
- - `onLinkClick?: (payload: { href: string; event: MouseEvent }) => void`: called with its `href` when a rendered Markdown link (`[text](url)`) is clicked. Pass a stable function (e.g. from `useCallback`). Ignored in source mode.
48
- - `onTagClick?: (payload: { tag: string; event: MouseEvent }) => void`: called with the tag name (without the leading `#`) when a rendered `#tag` is clicked. Pass a stable function (e.g. from `useCallback`). Ignored in source mode.
49
- - `resolveImageUrl?: (src: string) => string | undefined`: maps an image `src` to a displayable URL (or `undefined` to skip). Enables inline image rendering: `![alt](src)` stays literal text and the image renders beneath its line. Pass a stable function. Ignored in source mode.
50
- - `onImagePaste?: (file: File) => string | undefined | Promise<string | undefined>`: persists a pasted or dropped image file and returns its markdown `src` (or `undefined` to decline), synchronously or as a promise. Pass a stable function. Ignored in source mode.
51
- - `onImageSaveError?: (error: unknown, file: File) => void`: called when `onImagePaste` throws. Defaults to `console.error`. Ignored in source mode.
52
- - `onImageClick?: (payload: { src: string; alt: string; event: MouseEvent }) => void`: called when a rendered image is clicked, with its markdown `src`, `alt`, and the originating `MouseEvent`. Pass a stable function (e.g. from `useCallback`). Ignored in source mode.
53
- - `embedPaste?: boolean`: auto-embeds a pasted tweet or YouTube link as a rich embed; one undo turns the embed back into the raw link. On by default; set `false` to disable. Only takes effect when `resolveImageUrl` is set, since embeds render through the image pipeline. Ignored in source mode.
54
- - `bulletAfterHeading?: boolean`: pressing Enter at the end of the document's first heading (the title line) starts a fresh empty bullet on the next line instead of a plain paragraph. Off by default. Ignored in source mode.
55
- - `blockHandle?: boolean`: shows the per-block gutter handle in the rich modes (a drag grip for reordering blocks, plus the drop indicator). On by default; set `false` to hide the gutter affordance entirely, e.g. when the host does not want block reordering. Ignored in source mode and when `readOnly` is set.
56
- - `placeholder?: string | ((state) => string)`: placeholder text shown when the whole document is empty. Pass a stable function. Ignored in source mode.
57
- - `readOnly?: boolean`: makes the editor read-only, in both the rich and source modes.
58
- - `spellCheck?: boolean`: toggles the browser's native spell checking in the rich modes. Defaults to the browser's behavior. Ignored in source mode.
59
- - `editorClassName?: string`: class on the editable root (the contenteditable). Rich modes only.
60
- - `wrapperClassName?: string`: class on the outer `div.meowdown` wrapper.
61
- - `handleRef?: Ref<EditorHandle>`
62
- - `children?: ReactNode`: rendered inside the editor's ProseKit context, so children can call `useEditor()`. Only rendered in the rich modes; source mode ignores them.
63
-
64
- ### `<MarkdownView>`
65
-
66
- A read-only renderer that turns Markdown into a React tree styled exactly like `<MeowdownEditor>` in `hide` mode: inline marks, wiki-link chips, images, tweet/YouTube embeds, and syntax-highlighted code. It mounts no editor and holds no ProseMirror view, so it is cheap to render many times (backlink lists, search results, previews). Import `@meowdown/core/style.css` for the theme; the root carries the `ProseMirror` and `meowdown-content` classes plus `data-mark-mode`, so the editor stylesheet applies unchanged.
67
-
68
- ```tsx
69
- import '@meowdown/core/style.css'
70
- import { MarkdownView } from '@meowdown/react'
71
- ;<MarkdownView
72
- markdown="See [[Some Note]] and **bold**."
73
- onWikilinkClick={({ target }) => open(target)}
74
- />
75
- ```
76
-
77
- - `markdown: string`: the Markdown to render. Live: changing it re-renders.
78
- - `markMode?: 'hide' | 'focus' | 'show'`: defaults to `'hide'`.
79
- - `frontmatter?: boolean`: peel a leading YAML frontmatter block first. Off by default.
80
- - `resolveImageUrl?`, `onWikilinkClick?`, `onLinkClick?`, `onImageClick?`: the same shapes as the matching `<MeowdownEditor>` props. Pass stable functions.
81
- - `className?: string`: extra class on the content root.
82
-
83
- Code blocks render their content and syntax highlighting (the same `tok-*` classes as the editor) but without the editor's language picker / copy toolbar. `<MarkdownView>` requires a DOM environment; it is not a server-side HTML-string renderer.
84
-
85
- ### `useEditor`
86
-
87
- Re-exported from `@prosekit/react`. Call it from a component passed as `children` to read the live editor instance.
88
-
89
- ### `useKeymap`
90
-
91
- Re-exported from `@prosekit/react`. Registers a keymap on the editor from a `children` component; set its priority with `Priority` from `@meowdown/core`.
92
-
93
- ### `useExtension`
94
-
95
- Re-exported from `@prosekit/react`. Applies an extension to the editor from a `children` component.
96
-
97
- ### `EditorHandle`
98
-
99
- Imperative handle for the editor, attached via `handleRef`.
100
-
101
- - `getMarkdown(): string`: serializes the current document to Markdown. Can be expensive on large documents; call it on demand (e.g. throttled) instead of on every change.
102
- - `setMarkdown(markdown: string): void`: replaces the whole document as a single undoable edit. Does not fire `onDocChange`.
103
- - `getState(): EditorStateSnapshot`: returns `[markdown, selection]`, where `selection` is a `SelectionJSON` (`{ anchor: number, head: number, type: string }`).
104
- - `setState(markdown?: string, selection?: SelectionJSON | 'start' | 'end'): void`: replaces the document (if `markdown` is given) and restores `selection`: exactly when valid, otherwise clamped to the nearest text selection; out-of-range positions never throw. `'start'` and `'end'` jump to the document edges. Without a selection, the current one is mapped through the change. Restore a snapshot with `handle.setState(...handle.getState())`.
105
- - `getSelection(): SelectionJSON`: returns the current selection.
106
- - `setSelection(selection: SelectionJSON | 'start' | 'end'): void`: restores a selection with the same hint semantics as `setState`.
107
- - `focus(): void`: focuses the editor.
108
- - `scrollIntoView(): void`: scrolls the selection into view.
109
- - `editor: TypedEditor | undefined`: escape hatch for the underlying ProseKit editor, `undefined` in source mode. No stability guarantees beyond what `@meowdown/core` exports.
110
-
111
- Selection positions are in the mounted editor's coordinate space: ProseMirror document positions in the rich modes, character offsets in source mode. They round-trip within one mode but are not portable across a mode switch.
55
+ See the full API reference [here](https://npmx.dev/package-docs/@meowdown%2Freact/).
112
56
 
113
57
  ## Styling
114
58
 
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ReactElement, ReactNode, Ref } from "react";
2
- import { ImageClickHandler, ImageOptions, LinkClickHandler, MarkMode, PlaceholderOptions, TagClickHandler, TypedEditor, WikilinkClickHandler } from "@meowdown/core";
2
+ import { ExitBoundaryHandler, ImageClickHandler, ImageOptions, LinkClickHandler, MarkMode, PlaceholderOptions, TagClickHandler, TypedEditor, WikilinkClickHandler } from "@meowdown/core";
3
3
  import { SelectionJSON, SelectionJSON as SelectionJSON$1 } from "@prosekit/core";
4
4
  import { useEditor, useExtension, useKeymap } from "@prosekit/react";
5
5
 
@@ -130,6 +130,16 @@ interface EditorProps {
130
130
  * mode.
131
131
  */
132
132
  onTagClick?: TagClickHandler;
133
+ /**
134
+ * Called when the caret can move no further in the pressed arrow direction
135
+ * and leaves the document boundary: ArrowUp on the first visual line,
136
+ * ArrowDown on the last, or an arrow press on a selected node at the edge.
137
+ * Use it to move focus to a previous/next note or page. Receives the
138
+ * `direction` and the original `KeyboardEvent`. Return `false` to let the
139
+ * editor handle the key normally; any other return value consumes it. Pass a
140
+ * stable function (e.g. from `useCallback`). Ignored in source mode.
141
+ */
142
+ onExitBoundary?: ExitBoundaryHandler;
133
143
  /**
134
144
  * Maps an image `src` to a displayable URL, or `undefined` to skip that image.
135
145
  * Defaults to showing http(s) URLs as-is. Pass a stable function (e.g. from
@@ -200,6 +210,7 @@ declare function MeowdownEditor({
200
210
  onWikilinkClick,
201
211
  onLinkClick,
202
212
  onTagClick,
213
+ onExitBoundary,
203
214
  resolveImageUrl,
204
215
  onImagePaste,
205
216
  onImageSaveError,
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { Compartment, EditorSelection, EditorState } from "@codemirror/state";
7
7
  import { EditorView, keymap } from "@codemirror/view";
8
8
  import { clamp } from "@ocavue/utils";
9
9
  import { jsx, jsxs } from "react/jsx-runtime";
10
- import { codeBlockLanguages, defaultResolveImageUrl, defineBulletAfterHeading, defineEditorExtension, defineEmbedPaste, defineHTMLPaste, defineImage, defineImageClickHandler, defineLinkClickHandler, defineMarkMode, defineMarkdownCopy, definePlaceholder, defineReadonly, defineTagClickHandler, defineWikilinkClickHandler, defineWikilinkTrigger, docToMarkdown, getCodeTokens, getMarkBuilders, inlineTextToMarkChunks, listenForTweetHeight, markdownToDoc, matchEmbed } from "@meowdown/core";
10
+ import { codeBlockLanguages, defaultResolveImageUrl, defineBulletAfterHeading, defineEditorExtension, defineEmbedPaste, defineExitBoundaryHandler, defineHTMLPaste, defineImage, defineImageClickHandler, defineLinkClickHandler, defineMarkMode, defineMarkdownCopy, definePlaceholder, defineReadonly, defineTagClickHandler, defineWikilinkClickHandler, defineWikilinkTrigger, docToMarkdown, getCodeTokens, getMarkBuilders, inlineTextToMarkChunks, listenForTweetHeight, markdownToDoc, matchEmbed } from "@meowdown/core";
11
11
  import { canUseRegexLookbehind, createEditor, defineDocChangeHandler, union } from "@prosekit/core";
12
12
  import { Selection, TextSelection } from "@prosekit/pm/state";
13
13
  import { ProseKit, defineReactNodeView, useEditor, useEditor as useEditor$1, useEditorDerivedValue, useExtension, useExtension as useExtension$1, useKeymap } from "@prosekit/react";
@@ -320,7 +320,7 @@ function DropIndicator$1() {
320
320
 
321
321
  //#endregion
322
322
  //#region src/components/editor-extensions.tsx
323
- function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick, onTagClick, resolveImageUrl, onImagePaste, onImageSaveError, onImageClick, embedPaste, bulletAfterHeading, placeholder, readOnly, wikilinkEnabled }) {
323
+ function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick, onTagClick, onExitBoundary, resolveImageUrl, onImagePaste, onImageSaveError, onImageClick, embedPaste, bulletAfterHeading, placeholder, readOnly, wikilinkEnabled }) {
324
324
  useExtension$1(useMemo(() => {
325
325
  return defineMarkMode(markMode);
326
326
  }, [markMode]));
@@ -339,6 +339,9 @@ function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick,
339
339
  useExtension$1(useMemo(() => {
340
340
  return onTagClick ? defineTagClickHandler(onTagClick) : null;
341
341
  }, [onTagClick]));
342
+ useExtension$1(useMemo(() => {
343
+ return onExitBoundary ? defineExitBoundaryHandler(onExitBoundary) : null;
344
+ }, [onExitBoundary]));
342
345
  useExtension$1(useMemo(() => {
343
346
  return defineImage({
344
347
  resolveImageUrl,
@@ -777,7 +780,7 @@ function resolveSelection(doc, selection) {
777
780
  return TextSelection.between(doc.resolve(anchor), doc.resolve(head));
778
781
  }
779
782
  }
780
- function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTagSearch, onWikilinkSearch, onWikilinkClick, onLinkClick, onTagClick, resolveImageUrl, onImagePaste, onImageSaveError, onImageClick, embedPaste, bulletAfterHeading, frontmatter = false, blockHandle = true, placeholder, readOnly, spellCheck, editorClassName, ref, children }) {
783
+ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTagSearch, onWikilinkSearch, onWikilinkClick, onLinkClick, onTagClick, onExitBoundary, resolveImageUrl, onImagePaste, onImageSaveError, onImageClick, embedPaste, bulletAfterHeading, frontmatter = false, blockHandle = true, placeholder, readOnly, spellCheck, editorClassName, ref, children }) {
781
784
  const [editor] = useState(() => {
782
785
  const editor = createEditor({ extension: union(defineEditorExtension(), defineCodeBlockView()) });
783
786
  if (initialMarkdown) editor.setContent(markdownToDoc(initialMarkdown, {
@@ -860,6 +863,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
860
863
  onWikilinkClick,
861
864
  onLinkClick,
862
865
  onTagClick,
866
+ onExitBoundary,
863
867
  resolveImageUrl,
864
868
  onImagePaste,
865
869
  onImageSaveError,
@@ -883,7 +887,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
883
887
 
884
888
  //#endregion
885
889
  //#region src/components/editor.tsx
886
- function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onTagSearch, onWikilinkSearch, onWikilinkClick, onLinkClick, onTagClick, resolveImageUrl, onImagePaste, onImageSaveError, onImageClick, embedPaste = true, bulletAfterHeading = false, frontmatter = false, blockHandle = true, placeholder, readOnly, spellCheck, editorClassName, wrapperClassName, handleRef, children }) {
890
+ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onTagSearch, onWikilinkSearch, onWikilinkClick, onLinkClick, onTagClick, onExitBoundary, resolveImageUrl, onImagePaste, onImageSaveError, onImageClick, embedPaste = true, bulletAfterHeading = false, frontmatter = false, blockHandle = true, placeholder, readOnly, spellCheck, editorClassName, wrapperClassName, handleRef, children }) {
887
891
  const childRef = useRef(null);
888
892
  useImperativeHandle(handleRef, () => {
889
893
  function getMarkdown() {
@@ -950,6 +954,7 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onTagSea
950
954
  onWikilinkClick,
951
955
  onLinkClick,
952
956
  onTagClick,
957
+ onExitBoundary,
953
958
  resolveImageUrl,
954
959
  onImagePaste,
955
960
  onImageSaveError,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meowdown/react",
3
3
  "type": "module",
4
- "version": "0.23.0",
4
+ "version": "0.24.0",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -19,10 +19,10 @@
19
19
  ],
20
20
  "dependencies": {
21
21
  "@base-ui/react": "^1.6.0",
22
- "@codemirror/commands": "^6.10.3",
22
+ "@codemirror/commands": "^6.10.4",
23
23
  "@codemirror/lang-markdown": "^6.5.0",
24
24
  "@codemirror/language": "^6.12.3",
25
- "@codemirror/state": "^6.6.0",
25
+ "@codemirror/state": "^6.7.0",
26
26
  "@codemirror/view": "^6.43.1",
27
27
  "@ocavue/utils": "^1.7.0",
28
28
  "@prosekit/core": "^0.13.0-beta.3",
@@ -30,7 +30,7 @@
30
30
  "@prosekit/react": "^0.8.0-beta.11",
31
31
  "clsx": "^2.1.1",
32
32
  "lucide-react": "^1.21.0",
33
- "@meowdown/core": "0.23.0"
33
+ "@meowdown/core": "0.24.0"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "react": "^19.0.0",