@meowdown/react 0.30.1 → 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, defineSpellCheckPlugin, 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, spellCheck }) {
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,16 +239,14 @@ 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]));
@@ -572,21 +570,57 @@ var autocomplete_menu_module_default = {
572
570
  //#endregion
573
571
  //#region src/components/slash-menu.tsx
574
572
  const regex$2 = canUseRegexLookbehind() ? /(?<!\S)\/(\S.*)?$/u : /\/(\S.*)?$/u;
575
- function SlashMenuItem({ label, kbd, onSelect }) {
573
+ function SlashMenuItem({ label, detail, kbd, onSelect }) {
576
574
  return /* @__PURE__ */ jsxs(AutocompleteItem, {
575
+ value: label,
577
576
  className: autocomplete_menu_module_default.Item,
578
577
  onSelect,
579
- 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
+ ]
580
589
  });
581
590
  }
582
591
  function selectionInTableCell(editor) {
583
592
  return isSelectionInTableCell(editor.state);
584
593
  }
585
- function SlashMenu({ timeFormat = "12" }) {
594
+ function SlashMenu({ timeFormat = "12", onSlashMenuSearch }) {
586
595
  const editor = useEditor$1();
587
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
+ ]);
588
620
  return /* @__PURE__ */ jsx(AutocompleteRoot, {
589
621
  regex: regex$2,
622
+ onOpenChange: (event) => setOpen(event.detail),
623
+ onQueryChange: (event) => setQuery(event.detail),
590
624
  children: /* @__PURE__ */ jsx(AutocompletePositioner, {
591
625
  className: autocomplete_menu_module_default.Positioner,
592
626
  children: /* @__PURE__ */ jsxs(AutocompletePopup, {
@@ -657,6 +691,11 @@ function SlashMenu({ timeFormat = "12" }) {
657
691
  label: "Now",
658
692
  onSelect: () => editor.commands.insertText({ text: formatNowTime(timeFormat) })
659
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)),
660
699
  /* @__PURE__ */ jsx(AutocompleteEmpty, {
661
700
  className: autocomplete_menu_module_default.Item,
662
701
  children: "No results"
@@ -977,7 +1016,7 @@ function resolveSelection(doc, selection) {
977
1016
  return TextSelection.between(doc.resolve(anchor), doc.resolve(head));
978
1017
  }
979
1018
  }
980
- 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 }) {
981
1020
  const [editor] = useState(() => {
982
1021
  const editor = createEditor({ extension: union(defineEditorExtension(), defineCodeBlockView()) });
983
1022
  if (initialMarkdown) editor.setContent(markdownToDoc(initialMarkdown, {
@@ -1018,6 +1057,9 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
1018
1057
  function setMarkdown(markdown) {
1019
1058
  setState(markdown);
1020
1059
  }
1060
+ function insertMarkdown(markdown) {
1061
+ editor.commands.insertMarkdown(markdown);
1062
+ }
1021
1063
  function setSelection(selection) {
1022
1064
  setState(void 0, selection);
1023
1065
  }
@@ -1030,6 +1072,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
1030
1072
  return {
1031
1073
  getMarkdown,
1032
1074
  setMarkdown,
1075
+ insertMarkdown,
1033
1076
  getState,
1034
1077
  setState,
1035
1078
  getSelection,
@@ -1061,8 +1104,8 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
1061
1104
  onTagClick,
1062
1105
  onExitBoundary,
1063
1106
  resolveImageUrl,
1064
- onImagePaste,
1065
- onImageSaveError,
1107
+ onFilePaste,
1108
+ onFileSaveError,
1066
1109
  onImageClick,
1067
1110
  embedPaste,
1068
1111
  bulletAfterHeading,
@@ -1074,7 +1117,10 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
1074
1117
  blockHandle && !readOnly && /* @__PURE__ */ jsx(BlockHandle, {}),
1075
1118
  !readOnly && /* @__PURE__ */ jsx(TableHandle, {}),
1076
1119
  blockHandle && !readOnly && /* @__PURE__ */ jsx(DropIndicator$1, {}),
1077
- /* @__PURE__ */ jsx(SlashMenu, { timeFormat }),
1120
+ /* @__PURE__ */ jsx(SlashMenu, {
1121
+ timeFormat,
1122
+ onSlashMenuSearch
1123
+ }),
1078
1124
  !readOnly && /* @__PURE__ */ jsx(LinkMenu, {
1079
1125
  onLinkClick,
1080
1126
  onLinkCopy
@@ -1088,7 +1134,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
1088
1134
 
1089
1135
  //#endregion
1090
1136
  //#region src/components/editor.tsx
1091
- 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 }) {
1092
1138
  const childRef = useRef(null);
1093
1139
  useImperativeHandle(handleRef, () => {
1094
1140
  function getMarkdown() {
@@ -1097,6 +1143,9 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onTagSea
1097
1143
  function setMarkdown(markdown) {
1098
1144
  childRef.current?.setMarkdown(markdown);
1099
1145
  }
1146
+ function insertMarkdown(markdown) {
1147
+ childRef.current?.insertMarkdown(markdown);
1148
+ }
1100
1149
  function getState() {
1101
1150
  return childRef.current?.getState() ?? ["", {
1102
1151
  type: "text",
@@ -1126,6 +1175,7 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onTagSea
1126
1175
  return {
1127
1176
  getMarkdown,
1128
1177
  setMarkdown,
1178
+ insertMarkdown,
1129
1179
  getState,
1130
1180
  setState,
1131
1181
  getSelection,
@@ -1144,6 +1194,7 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onTagSea
1144
1194
  markMode: mode,
1145
1195
  initialMarkdown,
1146
1196
  onDocChange,
1197
+ onSlashMenuSearch,
1147
1198
  onTagSearch,
1148
1199
  onWikilinkSearch,
1149
1200
  onWikilinkClick,
@@ -1152,8 +1203,8 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onTagSea
1152
1203
  onTagClick,
1153
1204
  onExitBoundary,
1154
1205
  resolveImageUrl,
1155
- onImagePaste,
1156
- onImageSaveError,
1206
+ onFilePaste,
1207
+ onFileSaveError,
1157
1208
  onImageClick,
1158
1209
  embedPaste,
1159
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.1",
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.1"
28
+ "@meowdown/core": "0.31.0"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "react": "^19.0.0",