@meowdown/react 0.30.0 → 0.31.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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ReactElement, ReactNode, Ref } from "react";
2
- import { ExitBoundaryHandler, ImageClickHandler, ImageOptions, LinkClickHandler, LinkCopyHandler, MarkMode, PlaceholderOptions, TagClickHandler, TypedEditor, WikilinkClickHandler } from "@meowdown/core";
2
+ import { ExitBoundaryHandler, FilePasteOptions, ImageClickHandler, ImageOptions, LinkClickHandler, LinkCopyHandler, 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
 
@@ -22,6 +22,15 @@ interface EditorHandle {
22
22
  getMarkdown: () => string;
23
23
  /** Replaces the whole document as a single undoable edit. */
24
24
  setMarkdown: (markdown: string) => void;
25
+ /**
26
+ * Parses `markdown` and inserts it at the current selection as a single
27
+ * undoable edit, replacing any selected content. A lone paragraph is
28
+ * inserted inline at the cursor; anything else is inserted as blocks. The
29
+ * cursor lands at the end of the inserted content. An empty or
30
+ * whitespace-only string is a no-op. Unlike `setMarkdown`, it fires
31
+ * `onDocChange`: the host cannot know the resulting document.
32
+ */
33
+ insertMarkdown: (markdown: string) => void;
25
34
  /** Returns the current Markdown and selection. */
26
35
  getState: () => EditorStateSnapshot;
27
36
  /**
@@ -45,7 +54,23 @@ interface EditorHandle {
45
54
  */
46
55
  readonly editor: TypedEditor | undefined;
47
56
  }
57
+ /** One row of host items in the slash menu. The host ranks the rows; the menu does not re-sort. */
58
+ interface SlashMenuItem {
59
+ /** Stable row key; defaults to `label`. */
60
+ id?: string;
61
+ /** Display text, matched against the typed query like the built-in items. */
62
+ label: string;
63
+ /** Secondary text shown beside the label. */
64
+ detail?: string;
65
+ /** Runs after the menu closes and the typed `/query` text is removed. */
66
+ onSelect: () => void;
67
+ }
48
68
  /**
69
+ * Searches host items for the slash menu. Receives the query typed after `/`
70
+ * (lowercased, punctuation stripped; may be empty) and returns the rows to
71
+ * show after the built-in items, either synchronously or as a promise.
72
+ */
73
+ type SlashMenuSearchHandler = (query: string) => SlashMenuItem[] | Promise<SlashMenuItem[]>;
49
74
  /** One row in the tag menu. The host ranks the rows; the menu does not re-sort. */
50
75
  interface TagItem {
51
76
  /** Inserted as `#tag `. */
@@ -100,6 +125,16 @@ interface EditorProps {
100
125
  * `setState` on the handle do not fire it.
101
126
  */
102
127
  onDocChange?: VoidFunction;
128
+ /**
129
+ * Searches host items for the slash menu, which opens when typing `/`.
130
+ * Receives the query (lowercased, punctuation stripped, may be empty) and
131
+ * returns the items to show after the built-in ones, synchronously or as a
132
+ * promise. Selecting an item removes the typed `/query` text before its
133
+ * `onSelect` runs, so `onSelect` can insert at the cursor (e.g. via the
134
+ * handle's `insertMarkdown`). Pass a stable function (e.g. from
135
+ * `useCallback`). Omit to show only the built-in items.
136
+ */
137
+ onSlashMenuSearch?: SlashMenuSearchHandler;
103
138
  /**
104
139
  * Searches tags for the tag menu, which opens when typing `#` followed by
105
140
  * text. Receives the query (lowercased, punctuation stripped) and returns the
@@ -152,12 +187,13 @@ interface EditorProps {
152
187
  */
153
188
  resolveImageUrl?: ImageOptions['resolveImageUrl'];
154
189
  /**
155
- * Persists a pasted/dropped image file and returns its markdown `src`. Pass a
156
- * stable function.
190
+ * Persists a pasted/dropped file and returns its markdown destination,
191
+ * inserted as `![](src)` for an image and as a `[name](src)` link for any
192
+ * other file. Return `undefined` to decline. Pass a stable function.
157
193
  */
158
- onImagePaste?: ImageOptions['onImagePaste'];
159
- /** Called when persisting a pasted/dropped image throws. */
160
- onImageSaveError?: ImageOptions['onImageSaveError'];
194
+ onFilePaste?: FilePasteOptions['onFilePaste'];
195
+ /** Called when persisting a pasted/dropped file throws. */
196
+ onFileSaveError?: FilePasteOptions['onFileSaveError'];
161
197
  /**
162
198
  * Called when the user clicks a rendered image, with its markdown `src`,
163
199
  * `alt`, and the originating `MouseEvent`. Pass a stable function (e.g. from
@@ -215,6 +251,7 @@ declare function MeowdownEditor({
215
251
  mode,
216
252
  initialMarkdown,
217
253
  onDocChange,
254
+ onSlashMenuSearch,
218
255
  onTagSearch,
219
256
  onWikilinkSearch,
220
257
  onWikilinkClick,
@@ -223,8 +260,8 @@ declare function MeowdownEditor({
223
260
  onTagClick,
224
261
  onExitBoundary,
225
262
  resolveImageUrl,
226
- onImagePaste,
227
- onImageSaveError,
263
+ onFilePaste,
264
+ onFileSaveError,
228
265
  onImageClick,
229
266
  embedPaste,
230
267
  bulletAfterHeading,
@@ -281,4 +318,4 @@ declare function MarkdownView({
281
318
  className
282
319
  }: MarkdownViewProps): ReactElement;
283
320
  //#endregion
284
- export { type EditorHandle, type EditorMode, type EditorProps, type EditorStateSnapshot, MarkdownView, type MarkdownViewProps, MeowdownEditor, type SelectionHint, type SelectionJSON, type TagItem, type TagSearchHandler, type TimeFormat, type WikilinkItem, type WikilinkSearchHandler, useEditor, useExtension, useKeymap };
321
+ export { type EditorHandle, type EditorMode, type EditorProps, type EditorStateSnapshot, MarkdownView, type MarkdownViewProps, MeowdownEditor, type SelectionHint, type SelectionJSON, type SlashMenuItem, type SlashMenuSearchHandler, type TagItem, type TagSearchHandler, type TimeFormat, type WikilinkItem, type WikilinkSearchHandler, useEditor, useExtension, useKeymap };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { clsx } from "clsx/lite";
2
2
  import { Fragment, createElement, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
3
- import { codeBlockLanguages, defaultResolveImageUrl, defineBulletAfterHeading, defineEditorExtension, defineEmbedPaste, defineExitBoundaryHandler, defineHTMLPaste, defineImage, defineImageClickHandler, defineLinkClickHandler, defineLinkEditKeymap, defineLinkHoverHandler, defineMarkMode, defineMarkdownCopy, definePlaceholder, defineReadonly, defineTagClickHandler, defineWikilinkClickHandler, defineWikilinkTrigger, docToMarkdown, getCodeTokens, getMarkBuilders, getVirtualElementFromRange, inlineTextToMarkChunks, isSelectionInTableCell, listenForTweetHeight, markdownToDoc, matchEmbed } from "@meowdown/core";
3
+ import { codeBlockLanguages, defaultResolveImageUrl, defineBulletAfterHeading, defineEditorExtension, defineEmbedPaste, defineExitBoundaryHandler, defineFilePaste, defineHTMLPaste, defineImage, defineImageClickHandler, defineLinkClickHandler, defineLinkEditKeymap, defineLinkHoverHandler, defineMarkMode, defineMarkdownCopy, definePlaceholder, defineReadonly, defineSpellCheckPlugin, defineTagClickHandler, defineWikilinkClickHandler, defineWikilinkTrigger, docToMarkdown, getCodeTokens, getMarkBuilders, getVirtualElementFromRange, inlineTextToMarkChunks, isSelectionInTableCell, listenForTweetHeight, markdownToDoc, matchEmbed } from "@meowdown/core";
4
4
  import { clamp } from "@ocavue/utils";
5
5
  import { canUseRegexLookbehind, createEditor, defineDocChangeHandler, union } from "@prosekit/core";
6
6
  import { Selection, TextSelection } from "@prosekit/pm/state";
@@ -216,7 +216,7 @@ function DropIndicator$1() {
216
216
 
217
217
  //#endregion
218
218
  //#region src/components/editor-extensions.tsx
219
- function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick, onTagClick, onExitBoundary, resolveImageUrl, onImagePaste, onImageSaveError, onImageClick, embedPaste, bulletAfterHeading, placeholder, readOnly, wikilinkEnabled }) {
219
+ function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick, onTagClick, onExitBoundary, resolveImageUrl, onFilePaste, onFileSaveError, onImageClick, embedPaste, bulletAfterHeading, placeholder, readOnly, wikilinkEnabled, spellCheck }) {
220
220
  useExtension$1(useMemo(() => {
221
221
  return defineMarkMode(markMode);
222
222
  }, [markMode]));
@@ -239,24 +239,26 @@ function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick,
239
239
  return onExitBoundary ? defineExitBoundaryHandler(onExitBoundary) : null;
240
240
  }, [onExitBoundary]));
241
241
  useExtension$1(useMemo(() => {
242
- return defineImage({
243
- resolveImageUrl,
244
- onImagePaste,
245
- onImageSaveError
246
- });
247
- }, [
248
- resolveImageUrl,
249
- onImagePaste,
250
- onImageSaveError
251
- ]));
242
+ return defineImage({ resolveImageUrl });
243
+ }, [resolveImageUrl]));
244
+ useExtension$1(useMemo(() => {
245
+ return onFilePaste ? defineFilePaste({
246
+ onFilePaste,
247
+ onFileSaveError
248
+ }) : null;
249
+ }, [onFilePaste, onFileSaveError]));
252
250
  useExtension$1(useMemo(() => {
253
251
  return onImageClick ? defineImageClickHandler(onImageClick) : null;
254
252
  }, [onImageClick]));
255
253
  useExtension$1(useMemo(() => {
256
254
  return embedPaste ? defineEmbedPaste() : null;
257
255
  }, [embedPaste]));
258
- useExtension$1(useMemo(() => defineHTMLPaste(), []));
259
- useExtension$1(useMemo(() => defineMarkdownCopy(), []));
256
+ useExtension$1(useMemo(() => {
257
+ return defineHTMLPaste();
258
+ }, []));
259
+ useExtension$1(useMemo(() => {
260
+ return defineMarkdownCopy();
261
+ }, []));
260
262
  useExtension$1(useMemo(() => {
261
263
  return bulletAfterHeading ? defineBulletAfterHeading() : null;
262
264
  }, [bulletAfterHeading]));
@@ -269,6 +271,9 @@ function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick,
269
271
  useExtension$1(useMemo(() => {
270
272
  return wikilinkEnabled ? defineWikilinkTrigger() : null;
271
273
  }, [wikilinkEnabled]));
274
+ useExtension$1(useMemo(() => {
275
+ return spellCheck == null ? null : defineSpellCheckPlugin(spellCheck);
276
+ }, [spellCheck]));
272
277
  return null;
273
278
  }
274
279
 
@@ -565,21 +570,57 @@ var autocomplete_menu_module_default = {
565
570
  //#endregion
566
571
  //#region src/components/slash-menu.tsx
567
572
  const regex$2 = canUseRegexLookbehind() ? /(?<!\S)\/(\S.*)?$/u : /\/(\S.*)?$/u;
568
- function SlashMenuItem({ label, kbd, onSelect }) {
573
+ function SlashMenuItem({ label, detail, kbd, onSelect }) {
569
574
  return /* @__PURE__ */ jsxs(AutocompleteItem, {
575
+ value: label,
570
576
  className: autocomplete_menu_module_default.Item,
571
577
  onSelect,
572
- children: [/* @__PURE__ */ jsx("span", { children: label }), kbd && /* @__PURE__ */ jsx("kbd", { children: kbd })]
578
+ children: [
579
+ /* @__PURE__ */ jsx("span", {
580
+ className: detail ? autocomplete_menu_module_default.Label : void 0,
581
+ children: label
582
+ }),
583
+ detail ? /* @__PURE__ */ jsx("span", {
584
+ className: autocomplete_menu_module_default.Detail,
585
+ children: detail
586
+ }) : null,
587
+ kbd && /* @__PURE__ */ jsx("kbd", { children: kbd })
588
+ ]
573
589
  });
574
590
  }
575
591
  function selectionInTableCell(editor) {
576
592
  return isSelectionInTableCell(editor.state);
577
593
  }
578
- function SlashMenu({ timeFormat = "12" }) {
594
+ function SlashMenu({ timeFormat = "12", onSlashMenuSearch }) {
579
595
  const editor = useEditor$1();
580
596
  const inTableCell = useEditorDerivedValue(selectionInTableCell);
597
+ const [open, setOpen] = useState(false);
598
+ const [query, setQuery] = useState("");
599
+ const [hostItems, setHostItems] = useState([]);
600
+ const fetchHostItems = useCallback(async (query, signal) => {
601
+ if (!onSlashMenuSearch || signal.aborted) return;
602
+ const result = await onSlashMenuSearch(query);
603
+ if (signal.aborted) return;
604
+ setHostItems(result);
605
+ }, [onSlashMenuSearch]);
606
+ useEffect(() => {
607
+ if (!open) return;
608
+ const controller = new AbortController();
609
+ queueMicrotask(() => {
610
+ fetchHostItems(query, controller.signal);
611
+ });
612
+ return () => {
613
+ controller.abort();
614
+ };
615
+ }, [
616
+ open,
617
+ query,
618
+ fetchHostItems
619
+ ]);
581
620
  return /* @__PURE__ */ jsx(AutocompleteRoot, {
582
621
  regex: regex$2,
622
+ onOpenChange: (event) => setOpen(event.detail),
623
+ onQueryChange: (event) => setQuery(event.detail),
583
624
  children: /* @__PURE__ */ jsx(AutocompletePositioner, {
584
625
  className: autocomplete_menu_module_default.Positioner,
585
626
  children: /* @__PURE__ */ jsxs(AutocompletePopup, {
@@ -650,6 +691,11 @@ function SlashMenu({ timeFormat = "12" }) {
650
691
  label: "Now",
651
692
  onSelect: () => editor.commands.insertText({ text: formatNowTime(timeFormat) })
652
693
  }),
694
+ hostItems.map((item) => /* @__PURE__ */ jsx(SlashMenuItem, {
695
+ label: item.label,
696
+ detail: item.detail,
697
+ onSelect: item.onSelect
698
+ }, item.id ?? item.label)),
653
699
  /* @__PURE__ */ jsx(AutocompleteEmpty, {
654
700
  className: autocomplete_menu_module_default.Item,
655
701
  children: "No results"
@@ -970,7 +1016,7 @@ function resolveSelection(doc, selection) {
970
1016
  return TextSelection.between(doc.resolve(anchor), doc.resolve(head));
971
1017
  }
972
1018
  }
973
- function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTagSearch, onWikilinkSearch, onWikilinkClick, onLinkClick, onLinkCopy, onTagClick, onExitBoundary, resolveImageUrl, onImagePaste, onImageSaveError, onImageClick, embedPaste, bulletAfterHeading, frontmatter = false, blockHandle = true, placeholder, readOnly, spellCheck, timeFormat, editorClassName, ref, children }) {
1019
+ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onWikilinkClick, onLinkClick, onLinkCopy, onTagClick, onExitBoundary, resolveImageUrl, onFilePaste, onFileSaveError, onImageClick, embedPaste, bulletAfterHeading, frontmatter = false, blockHandle = true, placeholder, readOnly, spellCheck, timeFormat, editorClassName, ref, children }) {
974
1020
  const [editor] = useState(() => {
975
1021
  const editor = createEditor({ extension: union(defineEditorExtension(), defineCodeBlockView()) });
976
1022
  if (initialMarkdown) editor.setContent(markdownToDoc(initialMarkdown, {
@@ -1011,6 +1057,9 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
1011
1057
  function setMarkdown(markdown) {
1012
1058
  setState(markdown);
1013
1059
  }
1060
+ function insertMarkdown(markdown) {
1061
+ editor.commands.insertMarkdown(markdown);
1062
+ }
1014
1063
  function setSelection(selection) {
1015
1064
  setState(void 0, selection);
1016
1065
  }
@@ -1023,6 +1072,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
1023
1072
  return {
1024
1073
  getMarkdown,
1025
1074
  setMarkdown,
1075
+ insertMarkdown,
1026
1076
  getState,
1027
1077
  setState,
1028
1078
  getSelection,
@@ -1044,7 +1094,6 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
1044
1094
  children: [
1045
1095
  /* @__PURE__ */ jsx("div", {
1046
1096
  ref: editor.mount,
1047
- spellCheck,
1048
1097
  className: clsx("meowdown-content", editorClassName)
1049
1098
  }),
1050
1099
  /* @__PURE__ */ jsx(EditorExtensions, {
@@ -1055,19 +1104,23 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
1055
1104
  onTagClick,
1056
1105
  onExitBoundary,
1057
1106
  resolveImageUrl,
1058
- onImagePaste,
1059
- onImageSaveError,
1107
+ onFilePaste,
1108
+ onFileSaveError,
1060
1109
  onImageClick,
1061
1110
  embedPaste,
1062
1111
  bulletAfterHeading,
1063
1112
  placeholder,
1064
1113
  readOnly,
1065
- wikilinkEnabled: !!onWikilinkSearch
1114
+ wikilinkEnabled: !!onWikilinkSearch,
1115
+ spellCheck
1066
1116
  }),
1067
1117
  blockHandle && !readOnly && /* @__PURE__ */ jsx(BlockHandle, {}),
1068
1118
  !readOnly && /* @__PURE__ */ jsx(TableHandle, {}),
1069
1119
  blockHandle && !readOnly && /* @__PURE__ */ jsx(DropIndicator$1, {}),
1070
- /* @__PURE__ */ jsx(SlashMenu, { timeFormat }),
1120
+ /* @__PURE__ */ jsx(SlashMenu, {
1121
+ timeFormat,
1122
+ onSlashMenuSearch
1123
+ }),
1071
1124
  !readOnly && /* @__PURE__ */ jsx(LinkMenu, {
1072
1125
  onLinkClick,
1073
1126
  onLinkCopy
@@ -1081,7 +1134,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
1081
1134
 
1082
1135
  //#endregion
1083
1136
  //#region src/components/editor.tsx
1084
- function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onTagSearch, onWikilinkSearch, onWikilinkClick, onLinkClick, onLinkCopy, onTagClick, onExitBoundary, resolveImageUrl, onImagePaste, onImageSaveError, onImageClick, embedPaste = true, bulletAfterHeading = false, frontmatter = false, blockHandle = true, placeholder, readOnly, spellCheck, timeFormat, editorClassName, wrapperClassName, handleRef, children }) {
1137
+ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onWikilinkClick, onLinkClick, onLinkCopy, onTagClick, onExitBoundary, resolveImageUrl, onFilePaste, onFileSaveError, onImageClick, embedPaste = true, bulletAfterHeading = false, frontmatter = false, blockHandle = true, placeholder, readOnly, spellCheck, timeFormat, editorClassName, wrapperClassName, handleRef, children }) {
1085
1138
  const childRef = useRef(null);
1086
1139
  useImperativeHandle(handleRef, () => {
1087
1140
  function getMarkdown() {
@@ -1090,6 +1143,9 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onTagSea
1090
1143
  function setMarkdown(markdown) {
1091
1144
  childRef.current?.setMarkdown(markdown);
1092
1145
  }
1146
+ function insertMarkdown(markdown) {
1147
+ childRef.current?.insertMarkdown(markdown);
1148
+ }
1093
1149
  function getState() {
1094
1150
  return childRef.current?.getState() ?? ["", {
1095
1151
  type: "text",
@@ -1119,6 +1175,7 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onTagSea
1119
1175
  return {
1120
1176
  getMarkdown,
1121
1177
  setMarkdown,
1178
+ insertMarkdown,
1122
1179
  getState,
1123
1180
  setState,
1124
1181
  getSelection,
@@ -1137,6 +1194,7 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onTagSea
1137
1194
  markMode: mode,
1138
1195
  initialMarkdown,
1139
1196
  onDocChange,
1197
+ onSlashMenuSearch,
1140
1198
  onTagSearch,
1141
1199
  onWikilinkSearch,
1142
1200
  onWikilinkClick,
@@ -1145,8 +1203,8 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onTagSea
1145
1203
  onTagClick,
1146
1204
  onExitBoundary,
1147
1205
  resolveImageUrl,
1148
- onImagePaste,
1149
- onImageSaveError,
1206
+ onFilePaste,
1207
+ onFileSaveError,
1150
1208
  onImageClick,
1151
1209
  embedPaste,
1152
1210
  bulletAfterHeading,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meowdown/react",
3
3
  "type": "module",
4
- "version": "0.30.0",
4
+ "version": "0.31.0",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -25,7 +25,7 @@
25
25
  "@prosekit/react": "^0.8.0-beta.15",
26
26
  "clsx": "^2.1.1",
27
27
  "lucide-react": "^1.21.0",
28
- "@meowdown/core": "0.30.0"
28
+ "@meowdown/core": "0.31.0"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "react": "^19.0.0",