@meowdown/react 0.31.0 → 0.33.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
@@ -54,6 +54,8 @@ export function App() {
54
54
 
55
55
  See the full API reference [here](https://npmx.dev/package-docs/@meowdown%2Freact/).
56
56
 
57
+ Slash menu host items can include `keywords` to match hidden terms without changing the displayed label.
58
+
57
59
  ## Styling
58
60
 
59
61
  Import both stylesheets: `@meowdown/core/style.css` (the editor theme and variables) and `@meowdown/react/style.css` (the component layout). The core theme is documented in [`@meowdown/core`](https://www.npmjs.com/package/@meowdown/core).
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ReactElement, ReactNode, Ref } from "react";
2
- import { ExitBoundaryHandler, FilePasteOptions, ImageClickHandler, ImageOptions, LinkClickHandler, LinkCopyHandler, MarkMode, PlaceholderOptions, TagClickHandler, TypedEditor, WikilinkClickHandler } from "@meowdown/core";
2
+ import { AcceptPendingReplacementOptions, ExitBoundaryHandler, FileClickHandler, FileLinkResolver, FilePasteOptions, FileViewOptions, ImageClickHandler, ImageOptions, LinkClickHandler, LinkCopyHandler, MarkMode, PendingReplacement, PendingReplacementOutcome, PlaceholderOptions, StartPendingReplacementOptions, 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
 
@@ -24,7 +24,8 @@ interface EditorHandle {
24
24
  setMarkdown: (markdown: string) => void;
25
25
  /**
26
26
  * Parses `markdown` and inserts it at the current selection as a single
27
- * undoable edit, replacing any selected content. A lone paragraph is
27
+ * undoable edit. An active selection collapses first and is never
28
+ * deleted - this is a host-initiated insert, not a paste. A lone paragraph is
28
29
  * inserted inline at the cursor; anything else is inserted as blocks. The
29
30
  * cursor lands at the end of the inserted content. An empty or
30
31
  * whitespace-only string is a no-op. Unlike `setMarkdown`, it fires
@@ -48,6 +49,33 @@ interface EditorHandle {
48
49
  focus: () => void;
49
50
  /** Scrolls the selection into view. */
50
51
  scrollIntoView: () => void;
52
+ /**
53
+ * The current selection as Markdown: block structure (list markers,
54
+ * headings) is serialized, and inline syntax is already literal text. A
55
+ * selection inside one textblock comes back as its bare text.
56
+ */
57
+ getSelectedText: () => string;
58
+ /**
59
+ * Opens the selection menu over the current selection. A no-op when the
60
+ * selection is empty or `onSelectionMenuSearch` is not set.
61
+ */
62
+ openSelectionMenu: () => void;
63
+ /**
64
+ * Stages a pending replacement over a document range. Returns `false` when
65
+ * the range is invalid. Calling it again resets the accumulated text, which
66
+ * is how a retry starts.
67
+ */
68
+ startPendingReplacement: (options: StartPendingReplacementOptions) => boolean;
69
+ /** Appends streamed text to the staged replacement. */
70
+ appendPendingReplacementText: (text: string) => void;
71
+ /**
72
+ * Applies the staged replacement to the document as a single edit. Pass a
73
+ * `mode` to override the staged placement for this accept (e.g. "Insert
74
+ * below" on a replace stage).
75
+ */
76
+ acceptPendingReplacement: (options?: AcceptPendingReplacementOptions) => void;
77
+ /** Clears the staged replacement without touching the document. */
78
+ discardPendingReplacement: () => void;
51
79
  /**
52
80
  * Escape hatch: the underlying ProseKit editor, or `undefined` when the
53
81
  * handle does not wrap one.
@@ -60,6 +88,8 @@ interface SlashMenuItem {
60
88
  id?: string;
61
89
  /** Display text, matched against the typed query like the built-in items. */
62
90
  label: string;
91
+ /** Extra match terms beyond the label; never displayed. */
92
+ keywords?: string[];
63
93
  /** Secondary text shown beside the label. */
64
94
  detail?: string;
65
95
  /** Runs after the menu closes and the typed `/query` text is removed. */
@@ -106,6 +136,34 @@ interface WikilinkItem {
106
136
  * a promise.
107
137
  */
108
138
  type WikilinkSearchHandler = (query: string) => WikilinkItem[] | Promise<WikilinkItem[]>;
139
+ /** The selection the selection menu was opened over. */
140
+ interface SelectionMenuContext {
141
+ /** The selected text, with block boundaries as blank lines. */
142
+ selectedText: string;
143
+ /** Start of the selection. */
144
+ from: number;
145
+ /** End of the selection. */
146
+ to: number;
147
+ }
148
+ /** One row in the selection menu. The host ranks the rows; the menu does not re-sort. */
149
+ interface SelectionMenuItem {
150
+ /** Stable identity for the row. */
151
+ id: string;
152
+ /** Display text. */
153
+ label: string;
154
+ /** Secondary text shown beside the label. */
155
+ detail?: string;
156
+ /** Runs when the row is picked, with the selection the menu was opened over. */
157
+ onSelect: (context: SelectionMenuContext) => void;
158
+ }
159
+ /**
160
+ * Searches commands for the selection menu. Receives the filter text typed in
161
+ * the menu (may be empty) and the selection the menu was opened over, and
162
+ * returns the rows to show, either synchronously or as a promise.
163
+ */
164
+ type SelectionMenuSearchHandler = (query: string, context: SelectionMenuContext) => SelectionMenuItem[] | Promise<SelectionMenuItem[]>;
165
+ /** Reports how a pending replacement ended and its final staged value. */
166
+ type PendingReplacementResolveHandler = (outcome: PendingReplacementOutcome, pending: PendingReplacement) => void;
109
167
  //#endregion
110
168
  //#region src/components/editor.d.ts
111
169
  type EditorMode = MarkMode;
@@ -151,13 +209,42 @@ interface EditorProps {
151
209
  */
152
210
  onWikilinkSearch?: WikilinkSearchHandler;
153
211
  /**
154
- * Called with the link target on click of a rendered wiki link. Pass a stable
155
- * function (e.g. from `useCallback`).
212
+ * Searches commands for the selection menu, which opens over a non-empty
213
+ * selection via `EditorHandle.openSelectionMenu()` or the selection
214
+ * affordance. Receives the filter text typed in the menu (may be empty) and
215
+ * the selection the menu was opened over, and returns the rows to show,
216
+ * synchronously or as a promise. The host ranks the rows; the menu does not
217
+ * re-sort. Pass a stable function (e.g. from `useCallback`). Omit to disable
218
+ * the selection menu.
219
+ */
220
+ onSelectionMenuSearch?: SelectionMenuSearchHandler;
221
+ /**
222
+ * Shows a small floating button on a non-empty text selection that opens the
223
+ * selection menu. On by default; only relevant when `onSelectionMenuSearch`
224
+ * is set. Ignored when `readOnly` is set.
225
+ */
226
+ selectionMenuAffordance?: boolean;
227
+ /**
228
+ * Extra controls rendered in the pending-replacement preview footer, next to
229
+ * the built-in Accept and Discard buttons (e.g. a retry button).
230
+ */
231
+ pendingReplacementActions?: ReactNode;
232
+ /**
233
+ * Called when a pending replacement ends, with the outcome ('accepted' or
234
+ * 'discarded') and the final staged value. Use it to stop a stream that is
235
+ * still appending. Pass a stable function (e.g. from `useCallback`).
236
+ */
237
+ onPendingReplacementResolve?: PendingReplacementResolveHandler;
238
+ /**
239
+ * Called with the link target on click of a rendered wiki link, or on
240
+ * `Mod-Enter` with the caret on one. Pass a stable function (e.g. from
241
+ * `useCallback`).
156
242
  */
157
243
  onWikilinkClick?: WikilinkClickHandler;
158
244
  /**
159
245
  * Called with the link `href` on click of a rendered Markdown link
160
- * (`[text](url)`). Pass a stable function (e.g. from `useCallback`).
246
+ * (`[text](url)`), or on `Mod-Enter` with the caret on one. Pass a stable
247
+ * function (e.g. from `useCallback`).
161
248
  */
162
249
  onLinkClick?: LinkClickHandler;
163
250
  /**
@@ -167,7 +254,8 @@ interface EditorProps {
167
254
  onLinkCopy?: LinkCopyHandler;
168
255
  /**
169
256
  * Called with the tag name (without the leading `#`) on click of a rendered
170
- * `#tag`. Pass a stable function (e.g. from `useCallback`).
257
+ * `#tag`, or on `Mod-Enter` with the caret on one. Pass a stable function
258
+ * (e.g. from `useCallback`).
171
259
  */
172
260
  onTagClick?: TagClickHandler;
173
261
  /**
@@ -186,6 +274,30 @@ interface EditorProps {
186
274
  * `useCallback`).
187
275
  */
188
276
  resolveImageUrl?: ImageOptions['resolveImageUrl'];
277
+ /**
278
+ * Claims a `[label](url)` link as a file: a claimed link renders as an
279
+ * inline pill (file icon, name, size) instead of a link, behaves as one
280
+ * caret unit, and reports clicks through `onFileClick` instead of
281
+ * `onLinkClick`. The markdown text is untouched. Must be pure (same link,
282
+ * same answer). Read once when the editor is created: later identity
283
+ * changes are ignored, like `initialMarkdown`.
284
+ */
285
+ resolveFileLink?: FileLinkResolver;
286
+ /**
287
+ * Resolves the metadata (file size in bytes) shown on a file pill, directly
288
+ * or as a promise; the pill renders immediately and fills the size in when
289
+ * the promise settles. May be called repeatedly for the same `href`, so
290
+ * cache in the host when resolving is expensive. Pass a stable function
291
+ * (e.g. from `useCallback`).
292
+ */
293
+ resolveFileInfo?: FileViewOptions['resolveFileInfo'];
294
+ /**
295
+ * Called when the user clicks a rendered file pill (or presses `Mod-Enter`
296
+ * with the caret on one), with its `href`, `name`, and the originating
297
+ * event. The host decides what a click does (e.g. open the file in the OS
298
+ * default app). Pass a stable function (e.g. from `useCallback`).
299
+ */
300
+ onFileClick?: FileClickHandler;
189
301
  /**
190
302
  * Persists a pasted/dropped file and returns its markdown destination,
191
303
  * inserted as `![](src)` for an image and as a `[name](src)` link for any
@@ -254,12 +366,19 @@ declare function MeowdownEditor({
254
366
  onSlashMenuSearch,
255
367
  onTagSearch,
256
368
  onWikilinkSearch,
369
+ onSelectionMenuSearch,
370
+ selectionMenuAffordance,
371
+ pendingReplacementActions,
372
+ onPendingReplacementResolve,
257
373
  onWikilinkClick,
258
374
  onLinkClick,
259
375
  onLinkCopy,
260
376
  onTagClick,
261
377
  onExitBoundary,
262
378
  resolveImageUrl,
379
+ resolveFileLink,
380
+ resolveFileInfo,
381
+ onFileClick,
263
382
  onFilePaste,
264
383
  onFileSaveError,
265
384
  onImageClick,
@@ -318,4 +437,4 @@ declare function MarkdownView({
318
437
  className
319
438
  }: MarkdownViewProps): ReactElement;
320
439
  //#endregion
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 };
440
+ export { type EditorHandle, type EditorMode, type EditorProps, type EditorStateSnapshot, MarkdownView, type MarkdownViewProps, MeowdownEditor, type PendingReplacementResolveHandler, type SelectionHint, type SelectionJSON, type SelectionMenuContext, type SelectionMenuItem, type SelectionMenuSearchHandler, type SlashMenuItem, type SlashMenuSearchHandler, type TagItem, type TagSearchHandler, type TimeFormat, type WikilinkItem, type WikilinkSearchHandler, useEditor, useExtension, useKeymap };
package/dist/index.js CHANGED
@@ -1,12 +1,12 @@
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, 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";
3
+ import { codeBlockLanguages, defaultResolveImageUrl, defineBulletAfterHeading, defineEditorExtension, defineEmbedPaste, defineExitBoundaryHandler, defineFileClickHandler, defineFilePaste, defineFileView, defineFollowLinkHandler, defineHTMLPaste, defineImage, defineImageClickHandler, defineLinkClickHandler, defineLinkEditKeymap, defineLinkHoverHandler, defineMarkMode, defineMarkdownCopy, definePendingReplacementHandler, definePlaceholder, defineReadonly, defineSpellCheckPlugin, defineTagClickHandler, defineWikilinkClickHandler, defineWikilinkTrigger, docToMarkdown, getCodeTokens, getMarkBuilders, getPendingReplacement, getSelectedText, getVirtualElementFromRange, inlineTextToMarkChunks, isSelectionInTableCell, listenForTweetHeight, markdownToDoc, matchEmbed } from "@meowdown/core";
4
4
  import { clamp } from "@ocavue/utils";
5
- import { canUseRegexLookbehind, createEditor, defineDocChangeHandler, union } from "@prosekit/core";
5
+ import { canUseRegexLookbehind, createEditor, defineDocChangeHandler, defineUpdateHandler, isTextSelection, union } from "@prosekit/core";
6
6
  import { Selection, TextSelection } from "@prosekit/pm/state";
7
7
  import { ProseKit, defineReactNodeView, useEditor, useEditor as useEditor$1, useEditorDerivedValue, useExtension, useExtension as useExtension$1, useKeymap } from "@prosekit/react";
8
8
  import { Combobox } from "@base-ui/react/combobox";
9
- import { CheckIcon, ChevronsUpDownIcon, CopyIcon, GripHorizontalIcon, GripVerticalIcon, PencilIcon, UnlinkIcon } from "lucide-react";
9
+ import { CheckIcon, ChevronsUpDownIcon, CopyIcon, GripHorizontalIcon, GripVerticalIcon, PencilIcon, SparklesIcon, UnlinkIcon } from "lucide-react";
10
10
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
11
11
  import { BlockHandleDraggable, BlockHandlePopup, BlockHandlePositioner, BlockHandleRoot } from "@prosekit/react/block-handle";
12
12
  import { DropIndicator } from "@prosekit/react/drop-indicator";
@@ -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, onFilePaste, onFileSaveError, onImageClick, embedPaste, bulletAfterHeading, placeholder, readOnly, wikilinkEnabled, spellCheck }) {
219
+ function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick, onTagClick, onExitBoundary, resolveImageUrl, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, bulletAfterHeading, placeholder, readOnly, wikilinkEnabled, spellCheck }) {
220
220
  useExtension$1(useMemo(() => {
221
221
  return defineMarkMode(markMode);
222
222
  }, [markMode]));
@@ -235,12 +235,31 @@ function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick,
235
235
  useExtension$1(useMemo(() => {
236
236
  return onTagClick ? defineTagClickHandler(onTagClick) : null;
237
237
  }, [onTagClick]));
238
+ useExtension$1(useMemo(() => {
239
+ return onWikilinkClick || onTagClick || onFileClick || onLinkClick ? defineFollowLinkHandler({
240
+ onWikilinkClick,
241
+ onTagClick,
242
+ onFileClick,
243
+ onLinkClick
244
+ }) : null;
245
+ }, [
246
+ onWikilinkClick,
247
+ onTagClick,
248
+ onFileClick,
249
+ onLinkClick
250
+ ]));
238
251
  useExtension$1(useMemo(() => {
239
252
  return onExitBoundary ? defineExitBoundaryHandler(onExitBoundary) : null;
240
253
  }, [onExitBoundary]));
241
254
  useExtension$1(useMemo(() => {
242
255
  return defineImage({ resolveImageUrl });
243
256
  }, [resolveImageUrl]));
257
+ useExtension$1(useMemo(() => {
258
+ return defineFileView({ resolveFileInfo });
259
+ }, [resolveFileInfo]));
260
+ useExtension$1(useMemo(() => {
261
+ return onFileClick ? defineFileClickHandler(onFileClick) : null;
262
+ }, [onFileClick]));
244
263
  useExtension$1(useMemo(() => {
245
264
  return onFilePaste ? defineFilePaste({
246
265
  onFilePaste,
@@ -540,6 +559,299 @@ function LinkMenu({ onLinkClick, onLinkCopy }) {
540
559
  return null;
541
560
  }
542
561
 
562
+ //#endregion
563
+ //#region src/components/pending-replacement-preview.module.css
564
+ var pending_replacement_preview_module_default = {
565
+ "AcceptButton": "meow_AcceptButton_DPJ5ia",
566
+ "Button": "meow_Button_DPJ5ia",
567
+ "Footer": "meow_Footer_DPJ5ia",
568
+ "Popup": "meow_Popup_DPJ5ia",
569
+ "Positioner": "meow_Positioner_DPJ5ia",
570
+ "Spacer": "meow_Spacer_DPJ5ia",
571
+ "Text": "meow_Text_DPJ5ia",
572
+ "Waiting": "meow_Waiting_DPJ5ia"
573
+ };
574
+
575
+ //#endregion
576
+ //#region src/components/pending-replacement-preview.tsx
577
+ /**
578
+ * The preview for a staged (pending) replacement: a popover anchored to the
579
+ * source range showing the accumulated text, with Accept and Discard controls
580
+ * plus a host-provided `actions` slot. Dismissing the popover (Escape or an
581
+ * outside press) discards the stage; the document is only touched on accept.
582
+ */
583
+ function PendingReplacementPreview({ actions, onResolve }) {
584
+ const editor = useEditor$1();
585
+ const [pending, setPending] = useState(null);
586
+ useExtension$1(useMemo(() => {
587
+ return definePendingReplacementHandler((event) => {
588
+ if (event.type === "update") setPending(event.pending);
589
+ else {
590
+ setPending(null);
591
+ onResolve?.(event.outcome, event.pending);
592
+ }
593
+ });
594
+ }, [onResolve]));
595
+ const from = pending?.from;
596
+ const to = pending?.to;
597
+ const anchor = useMemo(() => {
598
+ if (from == null || to == null) return;
599
+ return getVirtualElementFromRange(editor.view, {
600
+ from,
601
+ to
602
+ });
603
+ }, [
604
+ from,
605
+ to,
606
+ editor
607
+ ]);
608
+ if (!pending) return null;
609
+ const discard = () => {
610
+ editor.commands.discardPendingReplacement();
611
+ editor.focus();
612
+ };
613
+ const accept = () => {
614
+ editor.commands.acceptPendingReplacement();
615
+ editor.focus();
616
+ };
617
+ return /* @__PURE__ */ jsx(Popover.Root, {
618
+ open: true,
619
+ onOpenChange: (next) => {
620
+ if (!next) discard();
621
+ },
622
+ children: /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Positioner, {
623
+ anchor,
624
+ side: "bottom",
625
+ sideOffset: 8,
626
+ className: pending_replacement_preview_module_default.Positioner,
627
+ children: /* @__PURE__ */ jsxs(Popover.Popup, {
628
+ className: pending_replacement_preview_module_default.Popup,
629
+ "data-testid": "pending-replacement",
630
+ initialFocus: false,
631
+ finalFocus: false,
632
+ children: [/* @__PURE__ */ jsx("div", {
633
+ className: pending_replacement_preview_module_default.Text,
634
+ "data-testid": "pending-replacement-text",
635
+ children: pending.text || /* @__PURE__ */ jsx("span", {
636
+ className: pending_replacement_preview_module_default.Waiting,
637
+ children: "Waiting for text..."
638
+ })
639
+ }), /* @__PURE__ */ jsxs("div", {
640
+ className: pending_replacement_preview_module_default.Footer,
641
+ children: [
642
+ actions,
643
+ /* @__PURE__ */ jsx("span", { className: pending_replacement_preview_module_default.Spacer }),
644
+ /* @__PURE__ */ jsx("button", {
645
+ type: "button",
646
+ className: pending_replacement_preview_module_default.Button,
647
+ "data-testid": "pending-replacement-discard",
648
+ onClick: discard,
649
+ children: "Discard"
650
+ }),
651
+ /* @__PURE__ */ jsx("button", {
652
+ type: "button",
653
+ className: pending_replacement_preview_module_default.AcceptButton,
654
+ "data-testid": "pending-replacement-accept",
655
+ disabled: !pending.text.trim(),
656
+ onClick: accept,
657
+ children: "Accept"
658
+ })
659
+ ]
660
+ })]
661
+ })
662
+ }) })
663
+ });
664
+ }
665
+
666
+ //#endregion
667
+ //#region src/components/selection-menu.module.css
668
+ var selection_menu_module_default = {
669
+ "AffordanceButton": "meow_AffordanceButton_2_4Zjq",
670
+ "AffordancePopup": "meow_AffordancePopup_2_4Zjq",
671
+ "AffordancePositioner": "meow_AffordancePositioner_2_4Zjq",
672
+ "Detail": "meow_Detail_2_4Zjq",
673
+ "Empty": "meow_Empty_2_4Zjq",
674
+ "Input": "meow_Input_2_4Zjq",
675
+ "Item": "meow_Item_2_4Zjq",
676
+ "Label": "meow_Label_2_4Zjq",
677
+ "List": "meow_List_2_4Zjq",
678
+ "Popup": "meow_Popup_2_4Zjq",
679
+ "Positioner": "meow_Positioner_2_4Zjq"
680
+ };
681
+
682
+ //#endregion
683
+ //#region src/components/selection-menu.tsx
684
+ /**
685
+ * A command menu over the current selection: a popover with a filter input and
686
+ * host-supplied rows, anchored to the selected range. Opened imperatively (via
687
+ * `EditorHandle.openSelectionMenu`) or from the selection affordance, a small
688
+ * floating button that appears on a non-empty selection.
689
+ */
690
+ function SelectionMenu({ onSelectionMenuSearch, context, onOpen, onClose, affordance = true }) {
691
+ const editor = useEditor$1();
692
+ const [selection, setSelection] = useState();
693
+ useExtension$1(useMemo(() => {
694
+ return defineUpdateHandler((view) => {
695
+ const { from, to, empty } = view.state.selection;
696
+ const anchorable = !empty && isTextSelection(view.state.selection) && !getPendingReplacement(view.state);
697
+ setSelection((previous) => {
698
+ if (previous?.from === from && previous?.to === to && previous?.anchorable === anchorable) return previous;
699
+ return {
700
+ from,
701
+ to,
702
+ anchorable
703
+ };
704
+ });
705
+ });
706
+ }, []));
707
+ const close = useCallback(() => {
708
+ onClose();
709
+ editor.focus();
710
+ }, [onClose, editor]);
711
+ const menuAnchor = useMemo(() => {
712
+ if (!context) return;
713
+ return getVirtualElementFromRange(editor.view, {
714
+ from: context.from,
715
+ to: context.to
716
+ });
717
+ }, [context, editor]);
718
+ const showAffordance = affordance && !!!context && !!selection?.anchorable;
719
+ const affordanceVisible = useDelayedFlag(showAffordance, 250, 0);
720
+ const affordanceAnchor = useMemo(() => {
721
+ if (!showAffordance || !selection) return;
722
+ return getVirtualElementFromRange(editor.view, {
723
+ from: selection.to,
724
+ to: selection.to
725
+ });
726
+ }, [
727
+ showAffordance,
728
+ selection,
729
+ editor
730
+ ]);
731
+ if (context) return /* @__PURE__ */ jsx(Popover.Root, {
732
+ open: true,
733
+ onOpenChange: (next) => {
734
+ if (!next) close();
735
+ },
736
+ children: /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Positioner, {
737
+ anchor: menuAnchor,
738
+ side: "bottom",
739
+ sideOffset: 8,
740
+ className: selection_menu_module_default.Positioner,
741
+ children: /* @__PURE__ */ jsx(Popover.Popup, {
742
+ className: selection_menu_module_default.Popup,
743
+ "data-testid": "selection-menu",
744
+ finalFocus: false,
745
+ children: /* @__PURE__ */ jsx(SelectionMenuPopup, {
746
+ onSelectionMenuSearch,
747
+ context,
748
+ onClose: close
749
+ })
750
+ })
751
+ }) })
752
+ });
753
+ if (affordanceVisible && showAffordance) return /* @__PURE__ */ jsx(Popover.Root, {
754
+ open: true,
755
+ onOpenChange: () => {},
756
+ children: /* @__PURE__ */ jsx(Popover.Portal, { children: /* @__PURE__ */ jsx(Popover.Positioner, {
757
+ anchor: affordanceAnchor,
758
+ side: "bottom",
759
+ sideOffset: 4,
760
+ className: selection_menu_module_default.AffordancePositioner,
761
+ children: /* @__PURE__ */ jsx(Popover.Popup, {
762
+ className: selection_menu_module_default.AffordancePopup,
763
+ "data-testid": "selection-menu-affordance",
764
+ initialFocus: false,
765
+ finalFocus: false,
766
+ children: /* @__PURE__ */ jsx("button", {
767
+ type: "button",
768
+ className: selection_menu_module_default.AffordanceButton,
769
+ title: "Selection commands",
770
+ "aria-label": "Selection commands",
771
+ onPointerDown: (event) => event.preventDefault(),
772
+ onClick: onOpen,
773
+ children: /* @__PURE__ */ jsx(SparklesIcon, {})
774
+ })
775
+ })
776
+ }) })
777
+ });
778
+ return null;
779
+ }
780
+ /** The menu content. Mounted only while the menu is open, so its filter state
781
+ * resets naturally on close. */
782
+ function SelectionMenuPopup({ onSelectionMenuSearch, context, onClose }) {
783
+ const [query, setQuery] = useState("");
784
+ const [items, setItems] = useState([]);
785
+ const [loading, setLoading] = useState(false);
786
+ const [activeIndex, setActiveIndex] = useState(0);
787
+ const fetchItems = useCallback(async (query, signal) => {
788
+ if (signal.aborted) return;
789
+ setLoading(true);
790
+ const result = await onSelectionMenuSearch(query, context);
791
+ if (signal.aborted) return;
792
+ setItems(result);
793
+ setActiveIndex(0);
794
+ setLoading(false);
795
+ }, [onSelectionMenuSearch, context]);
796
+ useEffect(() => {
797
+ const controller = new AbortController();
798
+ queueMicrotask(() => {
799
+ fetchItems(query, controller.signal);
800
+ });
801
+ return () => {
802
+ controller.abort();
803
+ };
804
+ }, [query, fetchItems]);
805
+ const selectItem = useCallback((item) => {
806
+ onClose();
807
+ item.onSelect(context);
808
+ }, [context, onClose]);
809
+ function onInputKeyDown(event) {
810
+ if (event.key === "ArrowDown") {
811
+ event.preventDefault();
812
+ setActiveIndex((index) => Math.min(index + 1, Math.max(items.length - 1, 0)));
813
+ } else if (event.key === "ArrowUp") {
814
+ event.preventDefault();
815
+ setActiveIndex((index) => Math.max(index - 1, 0));
816
+ } else if (event.key === "Enter") {
817
+ event.preventDefault();
818
+ const item = items[activeIndex];
819
+ if (item) selectItem(item);
820
+ }
821
+ }
822
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("input", {
823
+ autoFocus: true,
824
+ className: selection_menu_module_default.Input,
825
+ value: query,
826
+ placeholder: "Filter commands...",
827
+ "data-testid": "selection-menu-input",
828
+ onChange: (event) => setQuery(event.target.value),
829
+ onKeyDown: onInputKeyDown
830
+ }), /* @__PURE__ */ jsxs("div", {
831
+ role: "listbox",
832
+ className: selection_menu_module_default.List,
833
+ children: [items.map((item, index) => /* @__PURE__ */ jsxs("button", {
834
+ type: "button",
835
+ role: "option",
836
+ "aria-selected": index === activeIndex,
837
+ className: selection_menu_module_default.Item,
838
+ "data-active": index === activeIndex || void 0,
839
+ onPointerEnter: () => setActiveIndex(index),
840
+ onClick: () => selectItem(item),
841
+ children: [/* @__PURE__ */ jsx("span", {
842
+ className: selection_menu_module_default.Label,
843
+ children: item.label
844
+ }), item.detail ? /* @__PURE__ */ jsx("span", {
845
+ className: selection_menu_module_default.Detail,
846
+ children: item.detail
847
+ }) : null]
848
+ }, item.id)), items.length === 0 ? /* @__PURE__ */ jsx("div", {
849
+ className: selection_menu_module_default.Empty,
850
+ children: loading ? "Loading..." : "No commands"
851
+ }) : null]
852
+ })] });
853
+ }
854
+
543
855
  //#endregion
544
856
  //#region src/utils/date-format.ts
545
857
  /** Formats the current wall-clock time for the `/now` slash command. */
@@ -570,9 +882,9 @@ var autocomplete_menu_module_default = {
570
882
  //#endregion
571
883
  //#region src/components/slash-menu.tsx
572
884
  const regex$2 = canUseRegexLookbehind() ? /(?<!\S)\/(\S.*)?$/u : /\/(\S.*)?$/u;
573
- function SlashMenuItem({ label, detail, kbd, onSelect }) {
885
+ function SlashMenuItem({ label, keywords, detail, kbd, onSelect }) {
574
886
  return /* @__PURE__ */ jsxs(AutocompleteItem, {
575
- value: label,
887
+ value: [label, ...keywords ?? []].join(" "),
576
888
  className: autocomplete_menu_module_default.Item,
577
889
  onSelect,
578
890
  children: [
@@ -693,6 +1005,7 @@ function SlashMenu({ timeFormat = "12", onSlashMenuSearch }) {
693
1005
  }),
694
1006
  hostItems.map((item) => /* @__PURE__ */ jsx(SlashMenuItem, {
695
1007
  label: item.label,
1008
+ keywords: item.keywords,
696
1009
  detail: item.detail,
697
1010
  onSelect: item.onSelect
698
1011
  }, item.id ?? item.label)),
@@ -1016,9 +1329,9 @@ function resolveSelection(doc, selection) {
1016
1329
  return TextSelection.between(doc.resolve(anchor), doc.resolve(head));
1017
1330
  }
1018
1331
  }
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 }) {
1332
+ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance = true, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, bulletAfterHeading, frontmatter = false, blockHandle = true, placeholder, readOnly, spellCheck, timeFormat, editorClassName, ref, children }) {
1020
1333
  const [editor] = useState(() => {
1021
- const editor = createEditor({ extension: union(defineEditorExtension(), defineCodeBlockView()) });
1334
+ const editor = createEditor({ extension: union(defineEditorExtension({ resolveFileLink }), defineCodeBlockView()) });
1022
1335
  if (initialMarkdown) editor.setContent(markdownToDoc(initialMarkdown, {
1023
1336
  nodes: editor.nodes,
1024
1337
  frontmatter
@@ -1026,6 +1339,21 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1026
1339
  return editor;
1027
1340
  });
1028
1341
  const suppressDocChangeRef = useRef(false);
1342
+ const [selectionMenuContext, setSelectionMenuContext] = useState();
1343
+ const hasSelectionMenu = !!onSelectionMenuSearch;
1344
+ const openSelectionMenu = useCallback(() => {
1345
+ const { state } = editor;
1346
+ const { from, to, empty } = state.selection;
1347
+ if (empty) return;
1348
+ setSelectionMenuContext({
1349
+ selectedText: getSelectedText(state),
1350
+ from,
1351
+ to
1352
+ });
1353
+ }, [editor]);
1354
+ const closeSelectionMenu = useCallback(() => {
1355
+ setSelectionMenuContext(void 0);
1356
+ }, []);
1029
1357
  useImperativeHandle(ref, () => {
1030
1358
  function getMarkdown() {
1031
1359
  return docToMarkdown(editor.state.doc, { frontmatter });
@@ -1069,6 +1397,25 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1069
1397
  function scrollIntoView() {
1070
1398
  editor.view.dispatch(editor.state.tr.scrollIntoView());
1071
1399
  }
1400
+ function getSelectedTextFromState() {
1401
+ return getSelectedText(editor.state);
1402
+ }
1403
+ function openSelectionMenuFromHandle() {
1404
+ if (!hasSelectionMenu) return;
1405
+ openSelectionMenu();
1406
+ }
1407
+ function startPendingReplacement(options) {
1408
+ return editor.commands.startPendingReplacement(options);
1409
+ }
1410
+ function appendPendingReplacementText(text) {
1411
+ editor.commands.appendPendingReplacementText(text);
1412
+ }
1413
+ function acceptPendingReplacement(options) {
1414
+ editor.commands.acceptPendingReplacement(options ?? {});
1415
+ }
1416
+ function discardPendingReplacement() {
1417
+ editor.commands.discardPendingReplacement();
1418
+ }
1072
1419
  return {
1073
1420
  getMarkdown,
1074
1421
  setMarkdown,
@@ -1079,9 +1426,20 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1079
1426
  setSelection,
1080
1427
  focus,
1081
1428
  scrollIntoView,
1429
+ getSelectedText: getSelectedTextFromState,
1430
+ openSelectionMenu: openSelectionMenuFromHandle,
1431
+ startPendingReplacement,
1432
+ appendPendingReplacementText,
1433
+ acceptPendingReplacement,
1434
+ discardPendingReplacement,
1082
1435
  editor
1083
1436
  };
1084
- }, [editor, frontmatter]);
1437
+ }, [
1438
+ editor,
1439
+ frontmatter,
1440
+ hasSelectionMenu,
1441
+ openSelectionMenu
1442
+ ]);
1085
1443
  const handleDocChange = useMemo(() => {
1086
1444
  if (!onDocChange) return;
1087
1445
  return () => {
@@ -1104,6 +1462,8 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1104
1462
  onTagClick,
1105
1463
  onExitBoundary,
1106
1464
  resolveImageUrl,
1465
+ resolveFileInfo,
1466
+ onFileClick,
1107
1467
  onFilePaste,
1108
1468
  onFileSaveError,
1109
1469
  onImageClick,
@@ -1127,6 +1487,17 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1127
1487
  }),
1128
1488
  onTagSearch && /* @__PURE__ */ jsx(TagMenu, { onTagSearch }),
1129
1489
  onWikilinkSearch && /* @__PURE__ */ jsx(WikilinkMenu, { onWikilinkSearch }),
1490
+ onSelectionMenuSearch && !readOnly && /* @__PURE__ */ jsx(SelectionMenu, {
1491
+ onSelectionMenuSearch,
1492
+ context: selectionMenuContext,
1493
+ onOpen: openSelectionMenu,
1494
+ onClose: closeSelectionMenu,
1495
+ affordance: selectionMenuAffordance
1496
+ }),
1497
+ !readOnly && /* @__PURE__ */ jsx(PendingReplacementPreview, {
1498
+ actions: pendingReplacementActions,
1499
+ onResolve: onPendingReplacementResolve
1500
+ }),
1130
1501
  children
1131
1502
  ]
1132
1503
  });
@@ -1134,7 +1505,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1134
1505
 
1135
1506
  //#endregion
1136
1507
  //#region src/components/editor.tsx
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 }) {
1508
+ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance = true, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste = true, bulletAfterHeading = false, frontmatter = false, blockHandle = true, placeholder, readOnly, spellCheck, timeFormat, editorClassName, wrapperClassName, handleRef, children }) {
1138
1509
  const childRef = useRef(null);
1139
1510
  useImperativeHandle(handleRef, () => {
1140
1511
  function getMarkdown() {
@@ -1172,6 +1543,24 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
1172
1543
  function scrollIntoView() {
1173
1544
  childRef.current?.scrollIntoView();
1174
1545
  }
1546
+ function getSelectedText() {
1547
+ return childRef.current?.getSelectedText() ?? "";
1548
+ }
1549
+ function openSelectionMenu() {
1550
+ childRef.current?.openSelectionMenu();
1551
+ }
1552
+ function startPendingReplacement(options) {
1553
+ return childRef.current?.startPendingReplacement(options) ?? false;
1554
+ }
1555
+ function appendPendingReplacementText(text) {
1556
+ childRef.current?.appendPendingReplacementText(text);
1557
+ }
1558
+ function acceptPendingReplacement(options) {
1559
+ childRef.current?.acceptPendingReplacement(options);
1560
+ }
1561
+ function discardPendingReplacement() {
1562
+ childRef.current?.discardPendingReplacement();
1563
+ }
1175
1564
  return {
1176
1565
  getMarkdown,
1177
1566
  setMarkdown,
@@ -1182,6 +1571,12 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
1182
1571
  setSelection,
1183
1572
  focus,
1184
1573
  scrollIntoView,
1574
+ getSelectedText,
1575
+ openSelectionMenu,
1576
+ startPendingReplacement,
1577
+ appendPendingReplacementText,
1578
+ acceptPendingReplacement,
1579
+ discardPendingReplacement,
1185
1580
  get editor() {
1186
1581
  return childRef.current?.editor;
1187
1582
  }
@@ -1197,12 +1592,19 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
1197
1592
  onSlashMenuSearch,
1198
1593
  onTagSearch,
1199
1594
  onWikilinkSearch,
1595
+ onSelectionMenuSearch,
1596
+ selectionMenuAffordance,
1597
+ pendingReplacementActions,
1598
+ onPendingReplacementResolve,
1200
1599
  onWikilinkClick,
1201
1600
  onLinkClick,
1202
1601
  onLinkCopy,
1203
1602
  onTagClick,
1204
1603
  onExitBoundary,
1205
1604
  resolveImageUrl,
1605
+ resolveFileLink,
1606
+ resolveFileInfo,
1607
+ onFileClick,
1206
1608
  onFilePaste,
1207
1609
  onFileSaveError,
1208
1610
  onImageClick,
package/dist/style.css CHANGED
@@ -344,6 +344,187 @@
344
344
  outline-offset: -1px;
345
345
  }
346
346
  }
347
+ .meow_Positioner_DPJ5ia {
348
+ z-index: 50;
349
+ width: min(24rem, 100vw - 1rem);
350
+ display: block;
351
+ }
352
+
353
+ .meow_Popup_DPJ5ia {
354
+ box-sizing: border-box;
355
+ border: 1px solid var(--meowdown-border);
356
+ background: var(--meowdown-popover-bg);
357
+ border-radius: .75rem;
358
+ flex-direction: column;
359
+ width: 100%;
360
+ font-size: .875rem;
361
+ display: flex;
362
+ box-shadow: 0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a;
363
+ }
364
+
365
+ .meow_Text_DPJ5ia {
366
+ white-space: pre-wrap;
367
+ overflow-wrap: break-word;
368
+ max-height: 14rem;
369
+ color: var(--meowdown-text);
370
+ padding: .625rem .75rem;
371
+ overflow-y: auto;
372
+ }
373
+
374
+ .meow_Waiting_DPJ5ia {
375
+ color: var(--meowdown-muted);
376
+ }
377
+
378
+ .meow_Footer_DPJ5ia {
379
+ border-top: 1px solid var(--meowdown-border);
380
+ align-items: center;
381
+ gap: .25rem;
382
+ padding: .375rem;
383
+ display: flex;
384
+ }
385
+
386
+ .meow_Spacer_DPJ5ia {
387
+ flex: 1;
388
+ }
389
+
390
+ .meow_Button_DPJ5ia {
391
+ color: var(--meowdown-text);
392
+ cursor: pointer;
393
+ border-radius: .5rem;
394
+ align-items: center;
395
+ padding: .25rem .625rem;
396
+ display: inline-flex;
397
+
398
+ &:hover {
399
+ background: var(--meowdown-popover-hover-bg);
400
+ }
401
+ }
402
+
403
+ .meow_AcceptButton_DPJ5ia {
404
+ color: var(--meowdown-popover-bg);
405
+ background: var(--meowdown-accent);
406
+ cursor: pointer;
407
+ border-radius: .5rem;
408
+ align-items: center;
409
+ padding: .25rem .625rem;
410
+ display: inline-flex;
411
+
412
+ &:hover {
413
+ filter: brightness(1.05);
414
+ }
415
+
416
+ &:disabled {
417
+ opacity: .5;
418
+ cursor: default;
419
+ }
420
+ }
421
+ .meow_Positioner_2_4Zjq {
422
+ z-index: 50;
423
+ width: min(18rem, 100vw - 1rem);
424
+ display: block;
425
+ }
426
+
427
+ .meow_Popup_2_4Zjq {
428
+ box-sizing: border-box;
429
+ border: 1px solid var(--meowdown-border);
430
+ background: var(--meowdown-popover-bg);
431
+ border-radius: .75rem;
432
+ flex-direction: column;
433
+ width: 100%;
434
+ padding: .25rem;
435
+ font-size: .875rem;
436
+ display: flex;
437
+ box-shadow: 0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a;
438
+ }
439
+
440
+ .meow_Input_2_4Zjq {
441
+ box-sizing: border-box;
442
+ width: 100%;
443
+ font: inherit;
444
+ color: var(--meowdown-text);
445
+ border: 1px solid var(--meowdown-border);
446
+ background: none;
447
+ border-radius: .5rem;
448
+ margin-bottom: .25rem;
449
+ padding: .375rem .5rem;
450
+
451
+ &:focus {
452
+ outline: 2px solid var(--meowdown-accent);
453
+ outline-offset: -1px;
454
+ }
455
+ }
456
+
457
+ .meow_List_2_4Zjq {
458
+ flex-direction: column;
459
+ max-height: 16rem;
460
+ display: flex;
461
+ overflow-y: auto;
462
+ }
463
+
464
+ .meow_Item_2_4Zjq {
465
+ text-align: left;
466
+ color: var(--meowdown-text);
467
+ cursor: pointer;
468
+ border-radius: .5rem;
469
+ align-items: baseline;
470
+ gap: .5rem;
471
+ padding: .375rem .5rem;
472
+ display: flex;
473
+
474
+ &[data-active] {
475
+ background: var(--meowdown-popover-hover-bg);
476
+ }
477
+ }
478
+
479
+ .meow_Label_2_4Zjq {
480
+ flex: none;
481
+ }
482
+
483
+ .meow_Detail_2_4Zjq {
484
+ white-space: nowrap;
485
+ text-overflow: ellipsis;
486
+ min-width: 0;
487
+ color: var(--meowdown-muted);
488
+ flex: 1;
489
+ font-size: .8125rem;
490
+ overflow: hidden;
491
+ }
492
+
493
+ .meow_Empty_2_4Zjq {
494
+ color: var(--meowdown-muted);
495
+ padding: .375rem .5rem;
496
+ }
497
+
498
+ .meow_AffordancePositioner_2_4Zjq {
499
+ z-index: 40;
500
+ display: block;
501
+ }
502
+
503
+ .meow_AffordancePopup_2_4Zjq {
504
+ display: flex;
505
+ }
506
+
507
+ .meow_AffordanceButton_2_4Zjq {
508
+ color: var(--meowdown-muted);
509
+ background: var(--meowdown-popover-bg);
510
+ border: 1px solid var(--meowdown-border);
511
+ cursor: pointer;
512
+ border-radius: .5rem;
513
+ justify-content: center;
514
+ align-items: center;
515
+ padding: .25rem;
516
+ display: inline-flex;
517
+
518
+ &:hover {
519
+ color: var(--meowdown-accent);
520
+ background: var(--meowdown-popover-hover-bg);
521
+ }
522
+
523
+ & svg {
524
+ width: .875rem;
525
+ height: .875rem;
526
+ }
527
+ }
347
528
  .meow_Positioner_Dqll0G {
348
529
  z-index: 50;
349
530
  width: min(24rem, 100vw - 1rem);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meowdown/react",
3
3
  "type": "module",
4
- "version": "0.31.0",
4
+ "version": "0.33.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.31.0"
28
+ "@meowdown/core": "0.33.0"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "react": "^19.0.0",