@meowdown/react 0.44.2 → 0.45.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
@@ -56,12 +56,33 @@ See the full API reference [here](https://npmx.dev/package-docs/@meowdown%2Freac
56
56
 
57
57
  Slash menu host items can include `keywords` to match hidden terms without changing the displayed label.
58
58
 
59
+ ### Wiki-link hover cards
60
+
61
+ Mount `WikilinkHoverCard` inside `MeowdownEditor` and render host-owned preview
62
+ content from the hovered wiki link's `target`. Returning `null` renders no
63
+ card.
64
+
65
+ ```tsx
66
+ <MeowdownEditor initialMarkdown="See [[Project plan]]">
67
+ <WikilinkHoverCard>
68
+ {(hit) => <LocalNotePreview key={hit.target} target={hit.target} />}
69
+ </WikilinkHoverCard>
70
+ </MeowdownEditor>
71
+ ```
72
+
73
+ For local-only passive preview content, render Markdown with interaction
74
+ disabled: the tree contains no anchors or focusable controls, and recognized
75
+ tweet and YouTube embeds are omitted before any image resolver runs. Supply a
76
+ resolver that accepts only trusted local image sources.
77
+
78
+ ```tsx
79
+ <MarkdownView markdown={markdown} interactive={false} resolveImageUrl={resolveLocalImageUrl} />
80
+ ```
81
+
59
82
  ## Styling
60
83
 
61
84
  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).
62
85
 
63
- Math (`$E=mc^2$` inline, `$$` blocks, and ` ```math ` fences) is compiled by KaTeX to native MathML.
64
-
65
86
  ## License
66
87
 
67
88
  MIT
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ReactElement, ReactNode, Ref } from "react";
2
- import { AcceptPendingReplacementOptions, ExitBoundaryHandler, FileClickHandler, FileLinkResolver, FilePasteOptions, FileViewOptions, ImageClickHandler, ImageOptions, LinkClickHandler, LinkCopyHandler, ListMarker, MarkMode, PendingReplacement, PendingReplacementOutcome, PlaceholderOptions, StartPendingReplacementOptions, TagClickHandler, TypedEditor, WikilinkClickHandler } from "@meowdown/core";
2
+ import { AcceptPendingReplacementOptions, ExitBoundaryHandler, FileClickHandler, FileLinkResolver, FilePasteOptions, FileViewOptions, ImageClickHandler, ImageOptions, LinkClickHandler, LinkCopyHandler, ListMarker, MarkMode, PendingReplacement, PendingReplacementOutcome, PlaceholderOptions, StartPendingReplacementOptions, TagClickHandler, TypedEditor, WikilinkClickHandler, WikilinkHoverHit } 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
  //#region src/utils/date-format.d.ts
@@ -408,9 +408,16 @@ interface MarkdownViewProps {
408
408
  markMode?: MarkMode;
409
409
  /** Peel a leading YAML frontmatter block before rendering. Off by default. */
410
410
  frontmatter?: boolean;
411
+ /**
412
+ * Whether rendered links, images, and task checkboxes can be activated.
413
+ * Defaults to `true`. When `false`, callbacks are ignored, the rendered tree
414
+ * contains no anchors or focusable task controls, and recognized tweet and
415
+ * YouTube embeds are omitted before any image resolver runs.
416
+ */
417
+ interactive?: boolean;
411
418
  /** Map an image `src` to a displayable URL, or `undefined` to skip it. */
412
419
  resolveImageUrl?: (src: string) => string | undefined;
413
- /** Called when a rendered wiki link is clicked. Pass a stable function. */
420
+ /** Called when a rendered wikilink is clicked. Pass a stable function. */
414
421
  onWikilinkClick?: WikilinkClickHandler;
415
422
  /** Called when a rendered Markdown link is clicked. Pass a stable function. */
416
423
  onLinkClick?: LinkClickHandler;
@@ -423,7 +430,7 @@ interface MarkdownViewProps {
423
430
  }
424
431
  /**
425
432
  * Render Markdown to a read-only React tree that looks exactly like the editor
426
- * in `hide` mark mode: inline marks, wiki-link chips, images, tweet/YouTube
433
+ * in `hide` mark mode: inline marks, wikilink chips, images, tweet/YouTube
427
434
  * embeds, and syntax-highlighted code. No editor, no ProseMirror view; just a
428
435
  * walk over `markdownToDoc`'s document reusing meowdown's own parse, mark logic,
429
436
  * and CSS (the root carries `ProseMirror` + `data-mark-mode` so the existing
@@ -432,6 +439,22 @@ interface MarkdownViewProps {
432
439
  * Callbacks (`onWikilinkClick`, etc.) should be stable; pass them via
433
440
  * `useCallback` to avoid re-rendering the whole tree.
434
441
  */
435
- declare function MarkdownView({ markdown, markMode, frontmatter, resolveImageUrl, onWikilinkClick, onLinkClick, onImageClick, onTaskClick, className }: MarkdownViewProps): ReactElement;
442
+ declare function MarkdownView({ markdown, markMode, frontmatter, interactive, resolveImageUrl, onWikilinkClick, onLinkClick, onImageClick, onTaskClick, className }: MarkdownViewProps): ReactElement;
443
+ //#endregion
444
+ //#region src/components/wikilink-hover-card.d.ts
445
+ /** Props for {@link WikilinkHoverCard}. */
446
+ interface WikilinkHoverCardProps {
447
+ /**
448
+ * Render the card body from the hovered wiki link. Returning `null` renders
449
+ * no card.
450
+ */
451
+ readonly children: (hit: WikilinkHoverHit) => ReactNode;
452
+ /** Optional class applied to the popup, after the default card surface. */
453
+ readonly className?: string;
454
+ }
455
+ /**
456
+ * Show host-rendered content after a 300ms dwell over a rendered wiki link.
457
+ */
458
+ declare function WikilinkHoverCard({ children, className }: WikilinkHoverCardProps): ReactNode;
436
459
  //#endregion
437
- 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 TaskClickHandler, type TaskClickPayload, type TimeFormat, type WikilinkItem, type WikilinkSearchHandler, useEditor, useExtension, useKeymap };
460
+ 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 TaskClickHandler, type TaskClickPayload, type TimeFormat, WikilinkHoverCard, type WikilinkHoverCardProps, 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, cloneElement, createElement, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
3
- import { buildFileMarkdown, codeBlockLanguages, defaultResolveImageUrl, defineBulletAfterHeading, defineCodeBlockPreviewPlugin, defineEditorExtension, defineEmbedPaste, defineExitBoundaryHandler, defineFileClickHandler, defineFilePaste, defineFileView, defineFollowLinkHandler, defineHTMLPaste, defineImage, defineImageClickHandler, defineLinkClickHandler, defineLinkEditKeymap, defineLinkHoverHandler, defineLinkPaste, defineMarkdownCopy, definePendingReplacementHandler, definePlaceholder, defineReadonly, defineSpellCheckPlugin, defineTagClickHandler, defineWikilinkClickHandler, defineWikilinkTrigger, docToMarkdown, getCodeTokens, getMarkBuilders, getPendingReplacement, getSelectedText, getTableColumnAlign, getVirtualElementFromRange, inlineTextToMarkChunks, isCodeBlockPreviewHiddenDecoration, isSelectionInTableCell, listenForTweetHeight, loadKaTeX, markdownToDoc, matchEmbed, renderMathInto } from "@meowdown/core";
3
+ import { buildFileMarkdown, codeBlockLanguages, defaultResolveImageUrl, defineBulletAfterHeading, defineCodeBlockPreviewPlugin, defineEditorExtension, defineEmbedPaste, defineExitBoundaryHandler, defineFileClickHandler, defineFilePaste, defineFileView, defineFollowLinkHandler, defineHTMLPaste, defineImage, defineImageClickHandler, defineLinkClickHandler, defineLinkEditKeymap, defineLinkHoverHandler, defineLinkPaste, defineMarkdownCopy, definePendingReplacementHandler, definePlaceholder, defineReadonly, defineSpellCheckPlugin, defineTagClickHandler, defineWikilinkClickHandler, defineWikilinkHoverHandler, defineWikilinkTrigger, docToMarkdown, getCodeTokens, getMarkBuilders, getPendingReplacement, getSelectedText, getTableColumnAlign, getVirtualElementFromRange, inlineTextToMarkChunks, isCodeBlockPreviewHiddenDecoration, isSelectionInTableCell, listenForTweetHeight, loadKaTeX, markdownToDoc, matchEmbed, renderMathInto } from "@meowdown/core";
4
4
  import { clamp } from "@ocavue/utils";
5
5
  import { canUseRegexLookbehind, createEditor, defineDocChangeHandler, defineUpdateHandler, isTextSelection, union } from "@prosekit/core";
6
6
  import { Selection, TextSelection } from "@prosekit/pm/state";
@@ -16,6 +16,7 @@ import { MenuItem, MenuPopup, MenuPositioner } from "@prosekit/react/menu";
16
16
  import { TableHandleColumnMenuRoot, TableHandleColumnMenuTrigger, TableHandleColumnPopup, TableHandleColumnPositioner, TableHandleDragPreview, TableHandleDropIndicator, TableHandleRoot, TableHandleRowMenuRoot, TableHandleRowMenuTrigger, TableHandleRowPopup, TableHandleRowPositioner } from "@prosekit/react/table-handle";
17
17
  import { Mark } from "@prosekit/pm/model";
18
18
  import { BOOLEAN, OVERLOADED_BOOLEAN, getPropertyInfo, possibleStandardNames } from "react-property";
19
+ import { PreviewCard } from "@base-ui/react/preview-card";
19
20
 
20
21
  //#region src/hooks/use-katex.ts
21
22
  /**
@@ -381,16 +382,16 @@ function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick,
381
382
  //#endregion
382
383
  //#region src/hooks/use-delayed-flag.ts
383
384
  /** Delay before the flag opens, in ms. */
384
- const OPEN_DELAY = 400;
385
+ const OPEN_DELAY$1 = 400;
385
386
  /** Grace before the flag closes, in ms. The window lets a pointer travel from
386
387
  * the hovered link onto the popover it anchors. */
387
- const CLOSE_DELAY = 300;
388
+ const CLOSE_DELAY$1 = 300;
388
389
  /**
389
390
  * Mirrors `value` into a boolean that flips true `openDelay`ms after `value`
390
391
  * becomes true and false `closeDelay`ms after it becomes false, cancelling any
391
392
  * pending flip on each change.
392
393
  */
393
- function useDelayedFlag(value, openDelay = OPEN_DELAY, closeDelay = CLOSE_DELAY) {
394
+ function useDelayedFlag(value, openDelay = OPEN_DELAY$1, closeDelay = CLOSE_DELAY$1) {
394
395
  const [flag, setFlag] = useState(false);
395
396
  const timerRef = useRef(void 0);
396
397
  useEffect(() => {
@@ -1942,7 +1943,13 @@ function outputSpecToReact(spec, content, context) {
1942
1943
  const [tag, attrs, rest] = normalized;
1943
1944
  const reactProps = { ...attributesToProps(attrs, tag) };
1944
1945
  reactProps.key = `${key} ${JSON.stringify(attrs)}`;
1945
- if (tag === "input" && attrs?.["type"] === "checkbox") reactProps.readOnly = true;
1946
+ if (tag === "input" && attrs?.["type"] === "checkbox") {
1947
+ reactProps.readOnly = true;
1948
+ if (!context.interactive) {
1949
+ reactProps.disabled = true;
1950
+ reactProps.tabIndex = -1;
1951
+ }
1952
+ }
1946
1953
  return createElement(tag, reactProps, ...rest.map((child) => outputSpecToReact(child, content, context)));
1947
1954
  }
1948
1955
  function WikilinkChip(props) {
@@ -1994,9 +2001,9 @@ function EmbedFrame({ embed }) {
1994
2001
  });
1995
2002
  }
1996
2003
  function ImagePreview(props) {
1997
- const { src, alt, width, resolveImageUrl, onImageClick } = props;
2004
+ const { src, alt, width, resolveImageUrl, onImageClick, interactive } = props;
1998
2005
  const embed = matchEmbed(src);
1999
- if (embed) return /* @__PURE__ */ jsx(EmbedFrame, { embed });
2006
+ if (embed) return interactive ? /* @__PURE__ */ jsx(EmbedFrame, { embed }) : null;
2000
2007
  const url = (resolveImageUrl ?? defaultResolveImageUrl)(src);
2001
2008
  if (!url) return null;
2002
2009
  return /* @__PURE__ */ jsx("span", {
@@ -2025,7 +2032,8 @@ function ImageView(props) {
2025
2032
  alt,
2026
2033
  width,
2027
2034
  resolveImageUrl: context.resolveImageUrl,
2028
- onImageClick: context.onImageClick
2035
+ onImageClick: context.onImageClick,
2036
+ interactive: context.interactive
2029
2037
  }), /* @__PURE__ */ jsx("span", {
2030
2038
  className: "md-image-view-content md-atom-view-content",
2031
2039
  children
@@ -2142,6 +2150,10 @@ function wrapMark(mark, children, context) {
2142
2150
  }
2143
2151
  case "mdLinkText": {
2144
2152
  const attrs = mark.attrs;
2153
+ if (!context.interactive) return /* @__PURE__ */ jsx("span", {
2154
+ className: "md-link",
2155
+ children
2156
+ });
2145
2157
  const handleClick = context.onLinkClick ? (event) => context.onLinkClick?.({
2146
2158
  href: attrs.href,
2147
2159
  event: event.nativeEvent
@@ -2239,7 +2251,7 @@ function renderBlock(node, context) {
2239
2251
  }
2240
2252
  /**
2241
2253
  * Render Markdown to a read-only React tree that looks exactly like the editor
2242
- * in `hide` mark mode: inline marks, wiki-link chips, images, tweet/YouTube
2254
+ * in `hide` mark mode: inline marks, wikilink chips, images, tweet/YouTube
2243
2255
  * embeds, and syntax-highlighted code. No editor, no ProseMirror view; just a
2244
2256
  * walk over `markdownToDoc`'s document reusing meowdown's own parse, mark logic,
2245
2257
  * and CSS (the root carries `ProseMirror` + `data-mark-mode` so the existing
@@ -2248,15 +2260,16 @@ function renderBlock(node, context) {
2248
2260
  * Callbacks (`onWikilinkClick`, etc.) should be stable; pass them via
2249
2261
  * `useCallback` to avoid re-rendering the whole tree.
2250
2262
  */
2251
- function MarkdownView({ markdown, markMode = "hide", frontmatter = false, resolveImageUrl, onWikilinkClick, onLinkClick, onImageClick, onTaskClick, className }) {
2263
+ function MarkdownView({ markdown, markMode = "hide", frontmatter = false, interactive = true, resolveImageUrl, onWikilinkClick, onLinkClick, onImageClick, onTaskClick, className }) {
2252
2264
  const content = useMemo(() => {
2253
2265
  const doc = markdownToDoc(markdown, { frontmatter });
2254
2266
  const context = {
2267
+ interactive,
2255
2268
  resolveImageUrl,
2256
- onWikilinkClick,
2257
- onLinkClick,
2258
- onImageClick,
2259
- onTaskClick,
2269
+ onWikilinkClick: interactive ? onWikilinkClick : void 0,
2270
+ onLinkClick: interactive ? onLinkClick : void 0,
2271
+ onImageClick: interactive ? onImageClick : void 0,
2272
+ onTaskClick: interactive ? onTaskClick : void 0,
2260
2273
  taskCounter: { value: 0 },
2261
2274
  keyCounter: { value: 0 }
2262
2275
  };
@@ -2264,6 +2277,7 @@ function MarkdownView({ markdown, markMode = "hide", frontmatter = false, resolv
2264
2277
  }, [
2265
2278
  markdown,
2266
2279
  frontmatter,
2280
+ interactive,
2267
2281
  resolveImageUrl,
2268
2282
  onWikilinkClick,
2269
2283
  onLinkClick,
@@ -2278,4 +2292,77 @@ function MarkdownView({ markdown, markMode = "hide", frontmatter = false, resolv
2278
2292
  }
2279
2293
 
2280
2294
  //#endregion
2281
- export { MarkdownView, MeowdownEditor, useEditor, useExtension, useKeymap };
2295
+ //#region src/components/wikilink-hover-card.module.css
2296
+ var wikilink_hover_card_module_default = {
2297
+ "Popup": "meow_Popup_HMEwgW",
2298
+ "Positioner": "meow_Positioner_HMEwgW",
2299
+ "Viewport": "meow_Viewport_HMEwgW"
2300
+ };
2301
+
2302
+ //#endregion
2303
+ //#region src/components/wikilink-hover-card.tsx
2304
+ const OPEN_DELAY = 300;
2305
+ const CLOSE_DELAY = 200;
2306
+ /**
2307
+ * Show host-rendered content after a 300ms dwell over a rendered wiki link.
2308
+ */
2309
+ function WikilinkHoverCard({ children, className }) {
2310
+ const [hit, setHit] = useState();
2311
+ const lastRectRef = useRef(null);
2312
+ const [displayed, setDisplayed] = useState();
2313
+ const [open, setOpen] = useState(false);
2314
+ const [hoverExtension] = useState(() => {
2315
+ return defineWikilinkHoverHandler((nextHit) => setHit(nextHit));
2316
+ });
2317
+ useExtension$1(hoverExtension);
2318
+ const getRect = useCallback(() => {
2319
+ const rect = hit?.element?.getBoundingClientRect();
2320
+ if (rect && rect.width > 0 && rect.height > 0) lastRectRef.current = rect;
2321
+ return lastRectRef.current || new DOMRect(0, 0, 0, 0);
2322
+ }, [hit]);
2323
+ const anchor = useMemo(() => {
2324
+ return { getBoundingClientRect: getRect };
2325
+ }, [getRect]);
2326
+ const hasDisplayed = !!displayed;
2327
+ useEffect(() => {
2328
+ if (!hit) {
2329
+ const timer = setTimeout(() => setOpen(false), CLOSE_DELAY);
2330
+ return () => clearTimeout(timer);
2331
+ }
2332
+ const timer = setTimeout(() => {
2333
+ setDisplayed(hit);
2334
+ setOpen(true);
2335
+ }, hasDisplayed ? 0 : OPEN_DELAY);
2336
+ return () => clearTimeout(timer);
2337
+ }, [hit, hasDisplayed]);
2338
+ const body = displayed ? children(displayed) : null;
2339
+ return /* @__PURE__ */ jsx(PreviewCard.Root, {
2340
+ open: open && body != null,
2341
+ onOpenChange: (nextOpen) => {
2342
+ if (!nextOpen) setOpen(false);
2343
+ },
2344
+ onOpenChangeComplete: (nextOpen) => {
2345
+ if (!nextOpen) setDisplayed(void 0);
2346
+ },
2347
+ children: body != null && /* @__PURE__ */ jsx(PreviewCard.Portal, { children: /* @__PURE__ */ jsx(PreviewCard.Positioner, {
2348
+ anchor,
2349
+ side: "bottom",
2350
+ sideOffset: 8,
2351
+ collisionPadding: 8,
2352
+ className: wikilink_hover_card_module_default.Positioner,
2353
+ "data-testid": "wikilink-hover-positioner",
2354
+ children: /* @__PURE__ */ jsx(PreviewCard.Popup, {
2355
+ inert: true,
2356
+ className: clsx(wikilink_hover_card_module_default.Popup, className),
2357
+ "data-testid": "wikilink-hover-card",
2358
+ children: /* @__PURE__ */ jsx(PreviewCard.Viewport, {
2359
+ className: wikilink_hover_card_module_default.Viewport,
2360
+ children: body
2361
+ })
2362
+ })
2363
+ }) })
2364
+ });
2365
+ }
2366
+
2367
+ //#endregion
2368
+ export { MarkdownView, MeowdownEditor, WikilinkHoverCard, useEditor, useExtension, useKeymap };
package/dist/style.css CHANGED
@@ -764,6 +764,53 @@
764
764
  display: block;
765
765
  }
766
766
  }
767
+ .meow_Positioner_HMEwgW {
768
+ z-index: 50;
769
+ pointer-events: none;
770
+ max-width: calc(100vw - 1rem);
771
+ max-height: calc(100vh - 1rem);
772
+ transition-property: top, left, right, bottom, transform;
773
+ transition-duration: .35s;
774
+ transition-timing-function: cubic-bezier(.22, 1, .36, 1);
775
+ }
776
+
777
+ .meow_Popup_HMEwgW {
778
+ box-sizing: border-box;
779
+ pointer-events: none;
780
+ border: 1px solid var(--meowdown-border);
781
+ background: var(--meowdown-popover-bg);
782
+ width: min(20rem, 100vw - 1rem);
783
+ max-height: min(12rem, 100vh - 1rem);
784
+ transform-origin: var(--transform-origin);
785
+ border-radius: .75rem;
786
+ outline: none;
787
+ transition: opacity .15s, transform .15s;
788
+ display: flex;
789
+ box-shadow: 0 10px 15px -3px #0000001a, 0 4px 6px -4px #0000001a;
790
+ }
791
+
792
+ .meow_Popup_HMEwgW[data-closed] {
793
+ transition: opacity .1s, transform .1s;
794
+ }
795
+
796
+ .meow_Popup_HMEwgW[data-starting-style], .meow_Popup_HMEwgW[data-ending-style] {
797
+ opacity: 0;
798
+ transform: scale(.96);
799
+ }
800
+
801
+ .meow_Viewport_HMEwgW {
802
+ border-radius: inherit;
803
+ flex: 1;
804
+ min-width: 0;
805
+ position: relative;
806
+ overflow: clip;
807
+ }
808
+
809
+ @media (prefers-reduced-motion: reduce) {
810
+ .meow_Positioner_HMEwgW, .meow_Popup_HMEwgW {
811
+ transition: none;
812
+ }
813
+ }
767
814
  .meowdown {
768
815
  flex-direction: column;
769
816
  flex: 1 0 auto;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meowdown/react",
3
3
  "type": "module",
4
- "version": "0.44.2",
4
+ "version": "0.45.0",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -26,7 +26,7 @@
26
26
  "clsx": "^2.1.1",
27
27
  "lucide-react": "^1.21.0",
28
28
  "react-property": "^2.0.2",
29
- "@meowdown/core": "0.44.2"
29
+ "@meowdown/core": "0.45.0"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "react": "^19.0.0",