@meowdown/react 0.71.4 → 0.72.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
@@ -49,11 +49,12 @@ export function App() {
49
49
 
50
50
  ## Components
51
51
 
52
- | Component | Description |
53
- | ------------------- | -------------------------------------------------------------------------------------------------------------------------- |
54
- | `MeowdownEditor` | The editor. Callbacks and resolvers must be stable; pass them via `useCallback`. |
55
- | `MarkdownView` | Read-only Markdown renderer. `interactive={false}` renders passive content for previews. |
56
- | `WikilinkHoverCard` | Mount inside `MeowdownEditor`; renders host content for the hovered wiki link's `target`. Return `null` to render no card. |
52
+ | Component | Description |
53
+ | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
54
+ | `MeowdownEditor` | The editor. Callbacks and resolvers must be stable; pass them via `useCallback`. |
55
+ | `MarkdownView` | Read-only Markdown renderer. `interactive={false}` renders passive content for previews. |
56
+ | `WikilinkHoverCard` | Mount inside `MeowdownEditor`; renders host content for the hovered wiki link's `target`. Return `null` to render no card. |
57
+ | `LightboxRoot` / `LightboxImage` / `LightboxVideo` | Full-window image and video preview driven by `useLightbox()`. Compose your own close controls inside `LightboxRoot`, and call `lightbox.open(item, element)` from `onImageClick` or `onXPostMediaClick` to zoom from the clicked element. |
57
58
 
58
59
  Common `MeowdownEditor` props:
59
60
 
@@ -61,7 +62,7 @@ Common `MeowdownEditor` props:
61
62
  | ------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
62
63
  | `mode` | `'focus'` (default), `'show'`, or `'hide'`: how much Markdown syntax stays in view |
63
64
  | `searchQuery` / `onSearchChange` | Find in document, with `EditorHandle.findNext()` / `findPrevious()` |
64
- | `onWikilinkClick` / `onLinkClick` / `onTagClick` / `onImageClick` / `onFileClick` | Click handling for the rendered atoms |
65
+ | `onWikilinkClick` / `onLinkClick` / `onTagClick` / `onImageClick` / `onFileClick` / `onXPostMediaClick` | Click handling for the rendered atoms |
65
66
  | `resolveImageUrl` / `resolveWikiEmbed` / `resolveWikilink` / `resolveFileLink` / `resolveFileInfo` | Resolve and classify local content |
66
67
  | `onFilePaste` | Persist pasted or dropped files |
67
68
  | `onSlashMenuSearch` / `onTagSearch` / `onWikilinkSearch` / `onSelectionMenuSearch` | Search menus for `/`, `#`, `[[`, and selection commands |
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { ReactElement, ReactNode, Ref } from "react";
2
- import { AcceptPendingReplacementOptions, ExitBoundaryHandler, FileClickHandler, FileInfoResolver, FileLinkResolver, FilePasteOptions, FileViewOptions, ImageClickHandler, ImageOptions, LinkClickHandler, LinkCopyHandler, LinkPreview, LinkPreviewResolver, LinkPreviewResolver as LinkPreviewResolver$1, ListMarker, MarkMode, PendingReplacement, PendingReplacementOutcome, PlaceholderOptions, SearchStatusHandler, StartPendingReplacementOptions, TagClickHandler, TypedEditor, WikiEmbedResolver, WikilinkClickHandler, WikilinkHoverHit, WikilinkResolver, XPostResolver, XPostResolver as XPostResolver$1, YouTubeVideoResolver, YouTubeVideoResolver as YouTubeVideoResolver$1 } from "@meowdown/core";
1
+ import { ComponentProps, ReactElement, ReactNode, Ref } from "react";
2
+ import { AcceptPendingReplacementOptions, ExitBoundaryHandler, FileClickHandler, FileInfoResolver, FileLinkResolver, FilePasteOptions, FileViewOptions, ImageClickHandler, ImageOptions, LinkClickHandler, LinkCopyHandler, LinkPreview, LinkPreviewResolver, LinkPreviewResolver as LinkPreviewResolver$1, ListMarker, MarkMode, PendingReplacement, PendingReplacementOutcome, PlaceholderOptions, SearchStatusHandler, StartPendingReplacementOptions, TagClickHandler, TypedEditor, WikiEmbedResolver, WikilinkClickHandler, WikilinkHoverHit, WikilinkResolver, XPostMediaClickHandler, XPostResolver, XPostResolver as XPostResolver$1, YouTubeVideoResolver, YouTubeVideoResolver as YouTubeVideoResolver$1 } from "@meowdown/core";
3
3
  import { SelectionJSON, SelectionJSON as SelectionJSON$1 } from "@prosekit/core";
4
4
  import { ReactNodeViewComponent, ReactNodeViewComponent as ReactNodeViewComponent$1, useEditor, useExtension, useKeymap } from "@prosekit/react";
5
5
  //#region src/utils/date-format.d.ts
@@ -419,7 +419,7 @@ interface EditorProps {
419
419
  mediaUrlProtocols?: string[];
420
420
  /**
421
421
  * Resolves the data behind a YouTube video URL, directly or as a promise;
422
- * the video renders as a `post-embed-youtube-video` card. Defaults to
422
+ * the video renders as a `meowdown-embed-youtube` card. Defaults to
423
423
  * `defaultResolveYouTubeVideo`, which reads YouTube's oEmbed endpoint. Pass
424
424
  * a stable function (e.g. from `useCallback`).
425
425
  */
@@ -447,6 +447,15 @@ interface EditorProps {
447
447
  * Pass a stable function (e.g. from `useCallback`).
448
448
  */
449
449
  onImageClick?: ImageClickHandler;
450
+ /**
451
+ * Called with the `meowdown-embed-media-click` event when the user
452
+ * activates a photo or video inside an X post card. Its `detail` holds the
453
+ * item, its siblings, and the rendered thumbnail element. Call
454
+ * `event.preventDefault()` to stop the card from opening the photo URL or
455
+ * playing the video in place, then show the media yourself, for example in
456
+ * a lightbox. Pass a stable function (e.g. from `useCallback`).
457
+ */
458
+ onXPostMediaClick?: XPostMediaClickHandler;
450
459
  /**
451
460
  * Auto-embeds a pasted tweet or YouTube link as a rich embed; one undo turns
452
461
  * the embed back into the raw link. On by default.
@@ -545,7 +554,7 @@ interface EditorProps {
545
554
  *
546
555
  * Callbacks and resolvers should be stable; pass them via `useCallback`.
547
556
  */
548
- export declare function MeowdownEditor({ mode, initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, resolveLinkPreview, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveWikilink, resolveFileInfo, resolveXPost, mediaUrlProtocols, resolveYouTubeVideo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, linkPaste, bulletAfterHeading, substitution, frontmatter, blockHandle, caretGlide, placeholder, readOnly, spellCheck, searchQuery, onSearchChange, timeFormat, editorClassName, wrapperClassName, CodeBlockView, handleRef, children }: EditorProps): ReactElement;
557
+ export declare function MeowdownEditor({ mode, initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, resolveLinkPreview, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveWikilink, resolveFileInfo, resolveXPost, mediaUrlProtocols, resolveYouTubeVideo, onFileClick, onFilePaste, onFileSaveError, onImageClick, onXPostMediaClick, embedPaste, linkPaste, bulletAfterHeading, substitution, frontmatter, blockHandle, caretGlide, placeholder, readOnly, spellCheck, searchQuery, onSearchChange, timeFormat, editorClassName, wrapperClassName, CodeBlockView, handleRef, children }: EditorProps): ReactElement;
549
558
  //#endregion
550
559
  //#region src/components/markdown-view.d.ts
551
560
  /**
@@ -635,7 +644,7 @@ interface MarkdownViewProps {
635
644
  */
636
645
  resolveFileInfo?: FileInfoResolver;
637
646
  /**
638
- * Resolve the data behind an X post URL, rendered as a `post-embed-x-post`
647
+ * Resolve the data behind an X post URL, rendered as a `meowdown-embed-x`
639
648
  * card. Defaults to `defaultResolveXPost`.
640
649
  */
641
650
  resolveXPost?: XPostResolver$1;
@@ -645,7 +654,7 @@ interface MarkdownViewProps {
645
654
  mediaUrlProtocols?: string[];
646
655
  /**
647
656
  * Resolve the data behind a YouTube video URL, rendered as a
648
- * `post-embed-youtube-video` card. Defaults to `defaultResolveYouTubeVideo`.
657
+ * `meowdown-embed-youtube` card. Defaults to `defaultResolveYouTubeVideo`.
649
658
  */
650
659
  resolveYouTubeVideo?: YouTubeVideoResolver$1;
651
660
  /**
@@ -660,6 +669,12 @@ interface MarkdownViewProps {
660
669
  * Called when a rendered image is clicked. Pass a stable function.
661
670
  */
662
671
  onImageClick?: ImageClickHandler;
672
+ /**
673
+ * Called when a photo or video inside an X post card is activated. Call
674
+ * `event.preventDefault()` to stop the card from opening the photo URL or
675
+ * playing the video in place.
676
+ */
677
+ onXPostMediaClick?: XPostMediaClickHandler;
663
678
  /**
664
679
  * Called when a rendered file pill is clicked. Pass a stable function.
665
680
  */
@@ -684,7 +699,134 @@ interface MarkdownViewProps {
684
699
  * Callbacks (`onWikilinkClick`, etc.) and resolvers should be stable; pass them via
685
700
  * `useCallback` to avoid re-rendering the whole tree.
686
701
  */
687
- export declare function MarkdownView({ markdown, markMode, frontmatter, interactive, expandCollapsed, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveWikilink, resolveFileInfo, resolveXPost, mediaUrlProtocols, resolveYouTubeVideo, onWikilinkClick, onLinkClick, onImageClick, onFileClick, onTaskClick, className }: MarkdownViewProps): ReactElement;
702
+ export declare function MarkdownView({ markdown, markMode, frontmatter, interactive, expandCollapsed, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveWikilink, resolveFileInfo, resolveXPost, mediaUrlProtocols, resolveYouTubeVideo, onWikilinkClick, onLinkClick, onImageClick, onXPostMediaClick, onFileClick, onTaskClick, className }: MarkdownViewProps): ReactElement;
703
+ //#endregion
704
+ //#region src/hooks/use-lightbox.d.ts
705
+ /**
706
+ * Something a lightbox can show.
707
+ */
708
+ type LightboxItem = LightboxImageItem | LightboxVideoItem;
709
+ interface LightboxImageItem {
710
+ type: 'image';
711
+ /**
712
+ * A URL the browser can load, already resolved from the Markdown `src`.
713
+ */
714
+ src: string;
715
+ alt?: string | undefined;
716
+ }
717
+ interface LightboxVideoItem {
718
+ type: 'video';
719
+ /**
720
+ * In the order the browser should try them.
721
+ */
722
+ sources: Array<{
723
+ src: string;
724
+ type?: string | undefined;
725
+ }>;
726
+ /**
727
+ * URL of the image the player shows until the first video frame is ready.
728
+ */
729
+ poster?: string | undefined;
730
+ /**
731
+ * Width of the video itself, in pixels (not the size it is displayed at).
732
+ * With `height` it gives the player its aspect ratio before the video's
733
+ * metadata loads, so the open zoom ends on the right box.
734
+ */
735
+ width?: number | undefined;
736
+ /**
737
+ * Height of the video itself, in pixels.
738
+ */
739
+ height?: number | undefined;
740
+ /**
741
+ * Play like a GIF: muted, looping, without controls.
742
+ */
743
+ gif?: boolean | undefined;
744
+ alt?: string | undefined;
745
+ }
746
+ interface LightboxCloseOptions {
747
+ /**
748
+ * Close without the zoom back to the thumbnail, for example after the
749
+ * content was dragged off screen.
750
+ */
751
+ instant?: boolean | undefined;
752
+ }
753
+ /**
754
+ * State and commands for a {@link LightboxRoot}, from {@link useLightbox}.
755
+ */
756
+ interface LightboxController {
757
+ readonly item: LightboxItem | null;
758
+ /**
759
+ * Show `item`. Pass the clicked thumbnail as `element` to zoom the lightbox
760
+ * out of it, and back into it on close. The element only has to be in the
761
+ * document; React does not need to render it. Without an element, in a
762
+ * browser without View Transitions, or under reduced motion, the lightbox
763
+ * opens and closes without the zoom.
764
+ */
765
+ readonly open: (item: LightboxItem, element?: HTMLElement | null) => void;
766
+ readonly close: (options?: LightboxCloseOptions) => void;
767
+ /**
768
+ * The zoom back to the thumbnail is over.
769
+ *
770
+ * @internal
771
+ */
772
+ readonly onExited: () => void;
773
+ }
774
+ /**
775
+ * Owns which item a {@link LightboxRoot} shows. Opening and closing run as
776
+ * React transitions, so the `<ViewTransition>` inside the root animates them.
777
+ *
778
+ * The thumbnail is not rendered by this hook's component (it may not be
779
+ * rendered by React at all), so its half of the shared transition is set by
780
+ * hand: it carries the name while the browser captures the page without the
781
+ * lightbox, and loses it while the lightbox is mounted.
782
+ */
783
+ export declare function useLightbox(): LightboxController;
784
+ //#endregion
785
+ //#region src/components/lightbox-image.d.ts
786
+ /**
787
+ * Props for {@link LightboxImage}. Other props land on the `<img>`.
788
+ */
789
+ interface LightboxImageProps extends Omit<ComponentProps<'img'>, 'src' | 'alt'> {
790
+ readonly item: LightboxImageItem;
791
+ }
792
+ /**
793
+ * The image of a lightbox. It is the element the opened thumbnail zooms into.
794
+ */
795
+ export declare function LightboxImage({ item, className, style, ...props }: LightboxImageProps): ReactNode;
796
+ //#endregion
797
+ //#region src/components/lightbox-root.d.ts
798
+ /**
799
+ * Props for {@link LightboxRoot}. Other props land on the `<dialog>`.
800
+ */
801
+ interface LightboxRootProps extends Omit<ComponentProps<'dialog'>, 'children' | 'ref' | 'open' | 'onCancel' | 'onClose'> {
802
+ /**
803
+ * From {@link useLightbox}.
804
+ */
805
+ readonly lightbox: LightboxController;
806
+ /**
807
+ * Render the open item. Put a {@link LightboxImage} somewhere inside, along
808
+ * with whatever closes the lightbox.
809
+ */
810
+ readonly children: (item: LightboxItem) => ReactNode;
811
+ }
812
+ /**
813
+ * The full-window modal shell of a lightbox. It renders nothing while no item
814
+ * is open, and `Escape` closes it.
815
+ */
816
+ export declare function LightboxRoot({ lightbox, children, className, ...props }: LightboxRootProps): ReactNode;
817
+ //#endregion
818
+ //#region src/components/lightbox-video.d.ts
819
+ /**
820
+ * Props for {@link LightboxVideo}. Other props land on the `<video>`.
821
+ */
822
+ interface LightboxVideoProps extends Omit<ComponentProps<'video'>, 'src' | 'poster' | 'children'> {
823
+ readonly item: LightboxVideoItem;
824
+ }
825
+ /**
826
+ * The video of a lightbox. It starts playing as soon as it opens: the click
827
+ * that opened the lightbox is the user gesture that allows sound.
828
+ */
829
+ export declare function LightboxVideo({ item, className, style, ...props }: LightboxVideoProps): ReactNode;
688
830
  //#endregion
689
831
  //#region src/components/wikilink-hover-card.d.ts
690
832
  /**
@@ -711,4 +853,4 @@ interface WikilinkHoverCardProps {
711
853
  */
712
854
  export declare function WikilinkHoverCard({ children, className }: WikilinkHoverCardProps): ReactNode;
713
855
  //#endregion
714
- export { type EditorHandle, type EditorMode, type EditorProps, type EditorStateSnapshot, type LinkPreview, type LinkPreviewResolver, type MarkdownViewProps, type PendingReplacementResolveHandler, type ReactNodeViewComponent, 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 WikilinkHoverCardProps, type WikilinkItem, type WikilinkSearchHandler, type XPostResolver, type YouTubeVideoResolver, useEditor, useExtension, useKeymap };
856
+ export { type EditorHandle, type EditorMode, type EditorProps, type EditorStateSnapshot, type LightboxCloseOptions, type LightboxController, type LightboxImageItem, type LightboxImageProps, type LightboxItem, type LightboxRootProps, type LightboxVideoItem, type LightboxVideoProps, type LinkPreview, type LinkPreviewResolver, type MarkdownViewProps, type PendingReplacementResolveHandler, type ReactNodeViewComponent, 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 WikilinkHoverCardProps, type WikilinkItem, type WikilinkSearchHandler, type XPostResolver, type YouTubeVideoResolver, useEditor, useExtension, useKeymap };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { clsx } from "clsx/lite";
2
- import { Fragment, cloneElement, createElement, memo, useCallback, useDeferredValue, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
2
+ import { Fragment, ViewTransition, cloneElement, createElement, memo, startTransition, useCallback, useDeferredValue, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
3
3
  import { buildFileMarkdown, codeBlockLanguages, collectReferenceDefinitions, defaultResolveImageUrl, defaultResolveXPost, defaultResolveYouTubeVideo, defineCodeBlockPreviewPlugin, defineEditorExtension, defineLinkEditKeymap, defineLinkHoverHandler, definePendingReplacementHandler, defineSearchStatusHandler, defineVirtualCaret, defineWikilinkHoverHandler, docToMarkdown, formatFileSize, getCodeTokens, getFileKind, getLinkText, getMarkBuilders, getPendingReplacement, getSearchStatus, getSelectedText, getTableColumnAlign, getTextblockDisplayText, getVirtualElementFromRange, inlineTextToMarkChunksWithContext, isCodeBlockPreviewHiddenDecoration, isLinkTextForHref, isModEvent, isNodeOfType, isReferenceDefinitionNode, isSelectionInTableCell, loadKaTeX, markdownToDoc, normalizeHref, parsePostEmbedSnapshot, renderMathInto, updateEditorConfig } from "@meowdown/core";
4
4
  import { clamp } from "@ocavue/utils";
5
5
  import { canUseRegexLookbehind, createEditor, defineDocChangeHandler, defineUpdateHandler, isTextSelection, union } from "@prosekit/core";
@@ -15,9 +15,9 @@ import { Popover } from "@base-ui/react/popover";
15
15
  import { AutocompleteEmpty, AutocompleteItem, AutocompletePopup, AutocompletePositioner, AutocompleteRoot } from "@prosekit/react/autocomplete";
16
16
  import { MenuItem, MenuPopup, MenuPositioner } from "@prosekit/react/menu";
17
17
  import { TableHandleColumnMenuRoot, TableHandleColumnMenuTrigger, TableHandleColumnPopup, TableHandleColumnPositioner, TableHandleDragPreview, TableHandleDropIndicator, TableHandleRoot, TableHandleRowMenuRoot, TableHandleRowMenuTrigger, TableHandleRowPopup, TableHandleRowPositioner } from "@prosekit/react/table-handle";
18
+ import { X_POST_MEDIA_CLICK, registerXPost } from "@meowdown/embed/x";
19
+ import { registerYouTubeVideo } from "@meowdown/embed/youtube";
18
20
  import { matchEmbed } from "@meowdown/markdown";
19
- import { registerXPost } from "@post-embed/elements/x";
20
- import { registerYouTubeVideo } from "@post-embed/elements/youtube";
21
21
  import { Mark } from "@prosekit/pm/model";
22
22
  import { BOOLEAN, OVERLOADED_BOOLEAN, getPropertyInfo, possibleStandardNames } from "react-property";
23
23
  import { PreviewCard } from "@base-ui/react/preview-card";
@@ -1848,7 +1848,7 @@ function findHeadingPosition(doc, fragment) {
1848
1848
  });
1849
1849
  return match;
1850
1850
  }
1851
- function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance = true, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, resolveLinkPreview, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveWikilink, resolveFileInfo, resolveXPost, mediaUrlProtocols, resolveYouTubeVideo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, linkPaste, bulletAfterHeading, substitution = true, frontmatter = false, blockHandle = true, placeholder, readOnly, spellCheck, searchQuery = "", onSearchChange, timeFormat, editorClassName, CodeBlockView, ref, children }) {
1851
+ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance = true, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, resolveLinkPreview, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveWikilink, resolveFileInfo, resolveXPost, mediaUrlProtocols, resolveYouTubeVideo, onFileClick, onFilePaste, onFileSaveError, onImageClick, onXPostMediaClick, embedPaste, linkPaste, bulletAfterHeading, substitution = true, frontmatter = false, blockHandle = true, placeholder, readOnly, spellCheck, searchQuery = "", onSearchChange, timeFormat, editorClassName, CodeBlockView, ref, children }) {
1852
1852
  const suppressDocChangeRef = useRef(false);
1853
1853
  const handleDocChange = useMemo(() => {
1854
1854
  if (!onDocChange) return;
@@ -1876,6 +1876,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1876
1876
  onFilePaste,
1877
1877
  onFileSaveError,
1878
1878
  onImageClick,
1879
+ onXPostMediaClick,
1879
1880
  embedPaste,
1880
1881
  linkPaste,
1881
1882
  bulletAfterHeading,
@@ -1903,6 +1904,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1903
1904
  onFilePaste,
1904
1905
  onFileSaveError,
1905
1906
  onImageClick,
1907
+ onXPostMediaClick,
1906
1908
  embedPaste,
1907
1909
  linkPaste,
1908
1910
  bulletAfterHeading,
@@ -2105,7 +2107,7 @@ const CARET_GLIDE_OFF = { "--meowdown-caret-glide": "0ms" };
2105
2107
  *
2106
2108
  * Callbacks and resolvers should be stable; pass them via `useCallback`.
2107
2109
  */
2108
- function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance = true, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, resolveLinkPreview, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveWikilink, resolveFileInfo, resolveXPost, mediaUrlProtocols, resolveYouTubeVideo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste = true, linkPaste = true, bulletAfterHeading = false, substitution = true, frontmatter = false, blockHandle = true, caretGlide = true, placeholder, readOnly, spellCheck, searchQuery, onSearchChange, timeFormat, editorClassName, wrapperClassName, CodeBlockView, handleRef, children }) {
2110
+ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance = true, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, resolveLinkPreview, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveWikilink, resolveFileInfo, resolveXPost, mediaUrlProtocols, resolveYouTubeVideo, onFileClick, onFilePaste, onFileSaveError, onImageClick, onXPostMediaClick, embedPaste = true, linkPaste = true, bulletAfterHeading = false, substitution = true, frontmatter = false, blockHandle = true, caretGlide = true, placeholder, readOnly, spellCheck, searchQuery, onSearchChange, timeFormat, editorClassName, wrapperClassName, CodeBlockView, handleRef, children }) {
2109
2111
  const childRef = useRef(null);
2110
2112
  useImperativeHandle(handleRef, () => {
2111
2113
  function getMarkdown() {
@@ -2231,6 +2233,7 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
2231
2233
  onFilePaste,
2232
2234
  onFileSaveError,
2233
2235
  onImageClick,
2236
+ onXPostMediaClick,
2234
2237
  embedPaste,
2235
2238
  linkPaste,
2236
2239
  bulletAfterHeading,
@@ -2388,12 +2391,12 @@ function PostEmbed(props) {
2388
2391
  contentEditable: false,
2389
2392
  "data-testid": `${kind}-embed`,
2390
2393
  "data-post-embed": kind,
2391
- children: kind === "x-post" ? createElement("post-embed-x-post", {
2394
+ children: kind === "x-post" ? createElement("meowdown-embed-x", {
2392
2395
  data: null,
2393
2396
  url: src,
2394
2397
  resolver: resolveXPost ?? defaultResolveXPost,
2395
2398
  mediaUrlProtocols: mediaUrlProtocols ?? null
2396
- }) : createElement("post-embed-youtube-video", {
2399
+ }) : createElement("meowdown-embed-youtube", {
2397
2400
  data: saved?.kind === "youtube-video" ? saved.data : null,
2398
2401
  url: src,
2399
2402
  resolver: resolveYouTubeVideo,
@@ -2435,6 +2438,7 @@ function ImagePreview(props) {
2435
2438
  src,
2436
2439
  alt,
2437
2440
  event: event.nativeEvent,
2441
+ element: event.currentTarget,
2438
2442
  mod: isModEvent(event)
2439
2443
  });
2440
2444
  } : void 0,
@@ -2861,7 +2865,7 @@ const MarkdownBlock = memo(function MarkdownBlock({ node, taskBase, context, ref
2861
2865
  * Callbacks (`onWikilinkClick`, etc.) and resolvers should be stable; pass them via
2862
2866
  * `useCallback` to avoid re-rendering the whole tree.
2863
2867
  */
2864
- function MarkdownView({ markdown, markMode = "hide", frontmatter = false, interactive = true, expandCollapsed = false, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveWikilink, resolveFileInfo, resolveXPost, mediaUrlProtocols, resolveYouTubeVideo, onWikilinkClick, onLinkClick, onImageClick, onFileClick, onTaskClick, className }) {
2868
+ function MarkdownView({ markdown, markMode = "hide", frontmatter = false, interactive = true, expandCollapsed = false, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveWikilink, resolveFileInfo, resolveXPost, mediaUrlProtocols, resolveYouTubeVideo, onWikilinkClick, onLinkClick, onImageClick, onXPostMediaClick, onFileClick, onTaskClick, className }) {
2865
2869
  const context = useMemo(() => ({
2866
2870
  interactive,
2867
2871
  expandCollapsed,
@@ -2904,7 +2908,14 @@ function MarkdownView({ markdown, markMode = "hide", frontmatter = false, intera
2904
2908
  definitionsKey: definitionsSignature(referenceDefinitions)
2905
2909
  };
2906
2910
  }, [markdown, frontmatter]);
2911
+ const handleXPostMediaClick = interactive ? onXPostMediaClick : void 0;
2912
+ const rootRef = useCallback((root) => {
2913
+ if (!handleXPostMediaClick) return;
2914
+ root.addEventListener(X_POST_MEDIA_CLICK, handleXPostMediaClick);
2915
+ return () => root.removeEventListener(X_POST_MEDIA_CLICK, handleXPostMediaClick);
2916
+ }, [handleXPostMediaClick]);
2907
2917
  return /* @__PURE__ */ jsx("div", {
2918
+ ref: rootRef,
2908
2919
  className: clsx("ProseMirror", "meowdown-content", className),
2909
2920
  "data-mark-mode": markMode,
2910
2921
  children: blocks.map(({ node, taskBase }, index) => /* @__PURE__ */ jsx(MarkdownBlock, {
@@ -2917,6 +2928,161 @@ function MarkdownView({ markdown, markMode = "hide", frontmatter = false, intera
2917
2928
  });
2918
2929
  }
2919
2930
 
2931
+ //#endregion
2932
+ //#region src/hooks/use-lightbox.ts
2933
+ /**
2934
+ * The `view-transition-name` shared by the opened thumbnail and the lightbox
2935
+ * content, so the browser zooms between them.
2936
+ */
2937
+ const LIGHTBOX_TRANSITION_NAME = "meowdown-lightbox-media";
2938
+ function setTransitionName(element, name) {
2939
+ if (element) element.style.viewTransitionName = name;
2940
+ }
2941
+ /**
2942
+ * Owns which item a {@link LightboxRoot} shows. Opening and closing run as
2943
+ * React transitions, so the `<ViewTransition>` inside the root animates them.
2944
+ *
2945
+ * The thumbnail is not rendered by this hook's component (it may not be
2946
+ * rendered by React at all), so its half of the shared transition is set by
2947
+ * hand: it carries the name while the browser captures the page without the
2948
+ * lightbox, and loses it while the lightbox is mounted.
2949
+ */
2950
+ function useLightbox() {
2951
+ const [item, setItem] = useState(null);
2952
+ const sourceRef = useRef(null);
2953
+ const instantRef = useRef(false);
2954
+ const open = useCallback((nextItem, element) => {
2955
+ setTransitionName(sourceRef.current, "");
2956
+ sourceRef.current = element ?? null;
2957
+ setTransitionName(sourceRef.current, LIGHTBOX_TRANSITION_NAME);
2958
+ startTransition(() => setItem(nextItem));
2959
+ }, []);
2960
+ const close = useCallback((options) => {
2961
+ instantRef.current = options?.instant === true;
2962
+ if (instantRef.current) setItem(null);
2963
+ else startTransition(() => setItem(null));
2964
+ }, []);
2965
+ const isOpen = item != null;
2966
+ useLayoutEffect(() => {
2967
+ if (!isOpen) return;
2968
+ setTransitionName(sourceRef.current, "");
2969
+ return () => {
2970
+ const source = sourceRef.current;
2971
+ if (!instantRef.current && source?.isConnected) setTransitionName(source, LIGHTBOX_TRANSITION_NAME);
2972
+ else sourceRef.current = null;
2973
+ };
2974
+ }, [isOpen]);
2975
+ const onExited = useCallback(() => {
2976
+ setTransitionName(sourceRef.current, "");
2977
+ sourceRef.current = null;
2978
+ }, []);
2979
+ return useMemo(() => ({
2980
+ item,
2981
+ open,
2982
+ close,
2983
+ onExited
2984
+ }), [
2985
+ item,
2986
+ open,
2987
+ close,
2988
+ onExited
2989
+ ]);
2990
+ }
2991
+
2992
+ //#endregion
2993
+ //#region src/components/lightbox.module.css
2994
+ var lightbox_module_default = {
2995
+ "Dialog": "meow_Dialog_VENw2q",
2996
+ "Image": "meow_Image_VENw2q",
2997
+ "Video": "meow_Video_VENw2q"
2998
+ };
2999
+
3000
+ //#endregion
3001
+ //#region src/components/lightbox-image.tsx
3002
+ /**
3003
+ * The image of a lightbox. It is the element the opened thumbnail zooms into.
3004
+ */
3005
+ function LightboxImage({ item, className, style, ...props }) {
3006
+ return /* @__PURE__ */ jsx("img", {
3007
+ draggable: false,
3008
+ ...props,
3009
+ src: item.src,
3010
+ alt: item.alt ?? "",
3011
+ className: clsx(lightbox_module_default.Image, className),
3012
+ style: {
3013
+ ...style,
3014
+ viewTransitionName: LIGHTBOX_TRANSITION_NAME
3015
+ }
3016
+ });
3017
+ }
3018
+
3019
+ //#endregion
3020
+ //#region src/components/lightbox-root.tsx
3021
+ const DIALOG_TRANSITION_CLASS = "meowdown-lightbox-dialog";
3022
+ function showModal(dialog) {
3023
+ const active = document.activeElement;
3024
+ if (active instanceof HTMLElement && active.isContentEditable) active.blur();
3025
+ dialog.showModal();
3026
+ return () => dialog.close();
3027
+ }
3028
+ /**
3029
+ * The full-window modal shell of a lightbox. It renders nothing while no item
3030
+ * is open, and `Escape` closes it.
3031
+ */
3032
+ function LightboxRoot({ lightbox, children, className, ...props }) {
3033
+ const { item, close, onExited } = lightbox;
3034
+ if (!item) return null;
3035
+ return /* @__PURE__ */ jsx(ViewTransition, {
3036
+ default: DIALOG_TRANSITION_CLASS,
3037
+ onExit: () => onExited,
3038
+ children: /* @__PURE__ */ jsx("dialog", {
3039
+ "aria-label": item.type === "video" ? "Video preview" : "Image preview",
3040
+ ...props,
3041
+ ref: showModal,
3042
+ className: clsx(lightbox_module_default.Dialog, className),
3043
+ onCancel: (event) => {
3044
+ event.preventDefault();
3045
+ close();
3046
+ },
3047
+ onClose: (event) => {
3048
+ if (!event.currentTarget.open) close({ instant: true });
3049
+ },
3050
+ children: children(item)
3051
+ })
3052
+ });
3053
+ }
3054
+
3055
+ //#endregion
3056
+ //#region src/components/lightbox-video.tsx
3057
+ /**
3058
+ * The video of a lightbox. It starts playing as soon as it opens: the click
3059
+ * that opened the lightbox is the user gesture that allows sound.
3060
+ */
3061
+ function LightboxVideo({ item, className, style, ...props }) {
3062
+ const gif = item.gif === true;
3063
+ return /* @__PURE__ */ jsx("video", {
3064
+ controls: !gif,
3065
+ loop: gif,
3066
+ muted: gif,
3067
+ autoPlay: true,
3068
+ playsInline: true,
3069
+ "aria-label": item.alt,
3070
+ ...props,
3071
+ poster: item.poster,
3072
+ width: item.width,
3073
+ height: item.height,
3074
+ className: clsx(lightbox_module_default.Video, className),
3075
+ style: {
3076
+ ...style,
3077
+ viewTransitionName: LIGHTBOX_TRANSITION_NAME
3078
+ },
3079
+ children: item.sources.map((source) => /* @__PURE__ */ jsx("source", {
3080
+ src: source.src,
3081
+ type: source.type
3082
+ }, source.src))
3083
+ });
3084
+ }
3085
+
2920
3086
  //#endregion
2921
3087
  //#region src/components/wikilink-hover-card.module.css
2922
3088
  var wikilink_hover_card_module_default = {
@@ -2999,4 +3165,4 @@ function WikilinkHoverCard({ children, className }) {
2999
3165
  }
3000
3166
 
3001
3167
  //#endregion
3002
- export { MarkdownView, MeowdownEditor, WikilinkHoverCard, useEditor, useExtension, useKeymap };
3168
+ export { LightboxImage, LightboxRoot, LightboxVideo, MarkdownView, MeowdownEditor, WikilinkHoverCard, useEditor, useExtension, useKeymap, useLightbox };
package/dist/style.css CHANGED
@@ -1058,6 +1058,40 @@
1058
1058
  display: block;
1059
1059
  }
1060
1060
  }
1061
+ .meow_Dialog_VENw2q {
1062
+ all: revert;
1063
+ box-sizing: border-box;
1064
+ width: 100dvw;
1065
+ max-width: none;
1066
+ height: 100dvh;
1067
+ max-height: none;
1068
+ padding: var(--meowdown-lightbox-padding, 1.5rem);
1069
+ background: var(--meowdown-lightbox-backdrop, #000c);
1070
+ border: 0;
1071
+ outline: none;
1072
+ margin: 0;
1073
+ position: fixed;
1074
+ inset: 0;
1075
+ overflow: hidden;
1076
+
1077
+ &::backdrop {
1078
+ background: none;
1079
+ }
1080
+ }
1081
+
1082
+ .meow_Image_VENw2q {
1083
+ user-select: none;
1084
+ max-width: 100%;
1085
+ max-height: 100%;
1086
+ }
1087
+
1088
+ .meow_Video_VENw2q {
1089
+ width: auto;
1090
+ max-width: 100%;
1091
+ height: auto;
1092
+ max-height: 100%;
1093
+ position: relative;
1094
+ }
1061
1095
  .meow_Positioner_HMEwgW {
1062
1096
  z-index: 50;
1063
1097
  width: var(--positioner-width);
@@ -1114,3 +1148,28 @@
1114
1148
  flex: 1 0 auto;
1115
1149
  }
1116
1150
  }
1151
+
1152
+ ::view-transition-group(.meowdown-lightbox-dialog), ::view-transition-group(meowdown-lightbox-media) {
1153
+ animation-duration: var(--meowdown-lightbox-duration, .18s);
1154
+ animation-timing-function: cubic-bezier(.22, 1, .36, 1);
1155
+ }
1156
+
1157
+ ::view-transition-group(.meowdown-lightbox-dialog) {
1158
+ z-index: 1;
1159
+ }
1160
+
1161
+ ::view-transition-group(meowdown-lightbox-media) {
1162
+ z-index: 2;
1163
+ }
1164
+
1165
+ ::view-transition-old(meowdown-lightbox-media), ::view-transition-new(meowdown-lightbox-media) {
1166
+ object-fit: contain;
1167
+ height: 100%;
1168
+ overflow: clip;
1169
+ }
1170
+
1171
+ @media (prefers-reduced-motion: reduce) {
1172
+ ::view-transition-group(meowdown-lightbox-media), ::view-transition-old(meowdown-lightbox-media), ::view-transition-new(meowdown-lightbox-media), ::view-transition-group(.meowdown-lightbox-dialog), ::view-transition-old(.meowdown-lightbox-dialog), ::view-transition-new(.meowdown-lightbox-dialog) {
1173
+ animation: none;
1174
+ }
1175
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meowdown/react",
3
3
  "type": "module",
4
- "version": "0.71.4",
4
+ "version": "0.72.0",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -22,14 +22,14 @@
22
22
  ],
23
23
  "dependencies": {
24
24
  "@base-ui/react": "^1.8.0",
25
- "@meowdown/core": "0.72.1",
25
+ "@meowdown/core": "0.73.0",
26
+ "@meowdown/embed": "0.1.0",
26
27
  "@meowdown/markdown": "0.72.0",
27
28
  "@ocavue/utils": "^1.8.0",
28
- "@post-embed/elements": "^0.5.0",
29
- "@post-embed/types": "^0.4.0",
30
- "@prosekit/core": "^0.13.2",
31
- "@prosekit/pm": "^0.1.19",
32
- "@prosekit/react": "^0.8.2",
29
+ "@post-embed/types": "^0.5.0",
30
+ "@prosekit/core": "^0.13.3",
31
+ "@prosekit/pm": "^0.1.20",
32
+ "@prosekit/react": "^0.8.3",
33
33
  "beautiful-mermaid": "^1.1.3",
34
34
  "clsx": "^2.1.1",
35
35
  "github-slugger": "^2.0.0",
@@ -37,8 +37,8 @@
37
37
  "react-property": "^2.0.2"
38
38
  },
39
39
  "peerDependencies": {
40
- "react": "^19.0.0",
41
- "react-dom": "^19.0.0"
40
+ "react": "^19.3.0",
41
+ "react-dom": "^19.3.0"
42
42
  },
43
43
  "peerDependenciesMeta": {
44
44
  "react": {
@@ -61,7 +61,7 @@
61
61
  "react-dom": "^19.3.0",
62
62
  "tsdown": "^0.23.0",
63
63
  "vitest": "^5.0.1",
64
- "vitest-browser-commands": "^0.4.1",
64
+ "vitest-browser-commands": "^0.5.0",
65
65
  "vitest-browser-react": "^2.3.0"
66
66
  },
67
67
  "publishConfig": {