@mp-lb/mdkit 0.3.2 → 0.3.3

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.
Files changed (160) hide show
  1. package/README.md +8 -2
  2. package/dist/collaboration/useMdKitCollaboration.d.ts +5 -0
  3. package/dist/collaboration/useMdKitCollaboration.d.ts.map +1 -0
  4. package/dist/collaboration/useMdKitCollaboration.js +4 -0
  5. package/dist/core/checkpointPolicy.d.ts +10 -0
  6. package/dist/core/checkpointPolicy.d.ts.map +1 -0
  7. package/dist/core/checkpointPolicy.js +9 -0
  8. package/dist/core/documentEngine.d.ts +1 -0
  9. package/dist/core/documentEngine.d.ts.map +1 -0
  10. package/dist/core/index.d.ts +1 -0
  11. package/dist/core/index.d.ts.map +1 -0
  12. package/dist/document/MdKitConflictPanel.d.ts +5 -0
  13. package/dist/document/MdKitConflictPanel.d.ts.map +1 -0
  14. package/dist/document/MdKitConflictPanel.js +4 -0
  15. package/dist/document/MdKitDocumentToolbar.d.ts +6 -0
  16. package/dist/document/MdKitDocumentToolbar.d.ts.map +1 -0
  17. package/dist/document/MdKitDocumentToolbar.js +5 -0
  18. package/dist/document/documentTypes.d.ts +6 -0
  19. package/dist/document/documentTypes.d.ts.map +1 -0
  20. package/dist/document/useMdKitDocument.d.ts +5 -0
  21. package/dist/document/useMdKitDocument.d.ts.map +1 -0
  22. package/dist/document/useMdKitDocument.js +4 -0
  23. package/dist/fastify.d.ts +1 -0
  24. package/dist/fastify.d.ts.map +1 -0
  25. package/dist/index.d.ts +4 -1
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/markdown/MarkdownBubbleMenu.d.ts +1 -0
  28. package/dist/markdown/MarkdownBubbleMenu.d.ts.map +1 -0
  29. package/dist/markdown/MarkdownPasteExtension.d.ts +1 -0
  30. package/dist/markdown/MarkdownPasteExtension.d.ts.map +1 -0
  31. package/dist/markdown/MarkdownSearchExtension.d.ts +1 -0
  32. package/dist/markdown/MarkdownSearchExtension.d.ts.map +1 -0
  33. package/dist/markdown/MarkdownSearchPanel.d.ts +1 -0
  34. package/dist/markdown/MarkdownSearchPanel.d.ts.map +1 -0
  35. package/dist/markdown/MdKitEditor.d.ts +11 -0
  36. package/dist/markdown/MdKitEditor.d.ts.map +1 -0
  37. package/dist/markdown/MdKitEditor.js +10 -2
  38. package/dist/markdown/MdKitView.d.ts +9 -1
  39. package/dist/markdown/MdKitView.d.ts.map +1 -0
  40. package/dist/markdown/MdKitView.js +7 -2
  41. package/dist/markdown/TiptapMarkdownSurface.d.ts +1 -0
  42. package/dist/markdown/TiptapMarkdownSurface.d.ts.map +1 -0
  43. package/dist/markdown/TiptapMarkdownSurface.js +3 -22
  44. package/dist/markdown/createMdKitTiptapExtensions.d.ts +1 -0
  45. package/dist/markdown/createMdKitTiptapExtensions.d.ts.map +1 -0
  46. package/dist/markdown/editorDebug.d.ts +1 -0
  47. package/dist/markdown/editorDebug.d.ts.map +1 -0
  48. package/dist/markdown/markdownFenceRanges.d.ts +1 -0
  49. package/dist/markdown/markdownFenceRanges.d.ts.map +1 -0
  50. package/dist/markdown/normalizeMarkdownSerialization.d.ts +1 -0
  51. package/dist/markdown/normalizeMarkdownSerialization.d.ts.map +1 -0
  52. package/dist/markdown/prepareMarkdownForEditorHydration.d.ts +1 -0
  53. package/dist/markdown/prepareMarkdownForEditorHydration.d.ts.map +1 -0
  54. package/dist/markdown/preserveMarkdownWhitespace.d.ts +1 -0
  55. package/dist/markdown/preserveMarkdownWhitespace.d.ts.map +1 -0
  56. package/dist/markdown/yamlFrontMatter.d.ts +1 -0
  57. package/dist/markdown/yamlFrontMatter.d.ts.map +1 -0
  58. package/dist/server.d.ts +1 -0
  59. package/dist/server.d.ts.map +1 -0
  60. package/dist/theme/MdKitThemeEditor.d.ts +5 -0
  61. package/dist/theme/MdKitThemeEditor.d.ts.map +1 -0
  62. package/dist/theme/MdKitThemeEditor.js +4 -0
  63. package/dist/theme/editorTheme.d.ts +1 -0
  64. package/dist/theme/editorTheme.d.ts.map +1 -0
  65. package/dist/theme/editorTheme.js +8 -8
  66. package/dist/transport/backend.d.ts +13 -0
  67. package/dist/transport/backend.d.ts.map +1 -0
  68. package/dist/transport/backend.js +6 -0
  69. package/dist/transport/fastify.d.ts +5 -0
  70. package/dist/transport/fastify.d.ts.map +1 -0
  71. package/dist/transport/fastify.js +4 -0
  72. package/dist/transport/http.d.ts +1 -0
  73. package/dist/transport/http.d.ts.map +1 -0
  74. package/dist/transport/index.d.ts +1 -0
  75. package/dist/transport/index.d.ts.map +1 -0
  76. package/dist/transport/rest.d.ts +6 -0
  77. package/dist/transport/rest.d.ts.map +1 -0
  78. package/dist/transport/rest.js +5 -0
  79. package/dist/transport/store.d.ts +1 -0
  80. package/dist/transport/store.d.ts.map +1 -0
  81. package/dist/transport/trpcClient.d.ts +8 -0
  82. package/dist/transport/trpcClient.d.ts.map +1 -0
  83. package/dist/transport/trpcClient.js +7 -0
  84. package/dist/transport/trpcServer.d.ts +6 -0
  85. package/dist/transport/trpcServer.d.ts.map +1 -0
  86. package/dist/transport/trpcServer.js +5 -0
  87. package/dist/trpc/client.d.ts +1 -0
  88. package/dist/trpc/client.d.ts.map +1 -0
  89. package/dist/trpc/server.d.ts +1 -0
  90. package/dist/trpc/server.d.ts.map +1 -0
  91. package/dist/trpc.d.ts +1 -0
  92. package/dist/trpc.d.ts.map +1 -0
  93. package/dist/ui/joinClassNames.d.ts +1 -0
  94. package/dist/ui/joinClassNames.d.ts.map +1 -0
  95. package/dist/versioning/VersionHistoryPanel.d.ts +5 -0
  96. package/dist/versioning/VersionHistoryPanel.d.ts.map +1 -0
  97. package/dist/versioning/VersionHistoryPanel.js +4 -0
  98. package/dist/versioning/useMdKitDocumentVersions.d.ts +5 -0
  99. package/dist/versioning/useMdKitDocumentVersions.d.ts.map +1 -0
  100. package/dist/versioning/useMdKitDocumentVersions.js +4 -0
  101. package/dist/yjs/MdKitMarkdownYjs.d.ts +1 -0
  102. package/dist/yjs/MdKitMarkdownYjs.d.ts.map +1 -0
  103. package/dist/yjs/index.d.ts +1 -0
  104. package/dist/yjs/index.d.ts.map +1 -0
  105. package/package.json +10 -12
  106. package/src/collaboration/useMdKitCollaboration.ts +528 -0
  107. package/src/core/checkpointPolicy.ts +107 -0
  108. package/src/core/documentEngine.ts +175 -0
  109. package/src/core/index.ts +33 -0
  110. package/src/document/MdKitConflictPanel.tsx +129 -0
  111. package/src/document/MdKitDocumentToolbar.tsx +141 -0
  112. package/src/document/documentTypes.ts +89 -0
  113. package/src/document/useMdKitDocument.ts +543 -0
  114. package/src/fastify.ts +6 -0
  115. package/src/index.ts +89 -0
  116. package/src/markdown/MarkdownBubbleMenu.tsx +271 -0
  117. package/src/markdown/MarkdownPasteExtension.ts +81 -0
  118. package/src/markdown/MarkdownSearchExtension.ts +77 -0
  119. package/src/markdown/MarkdownSearchPanel.tsx +98 -0
  120. package/src/markdown/MdKitEditor.tsx +75 -0
  121. package/src/markdown/MdKitView.tsx +80 -0
  122. package/src/markdown/TiptapMarkdownSurface.tsx +923 -0
  123. package/src/markdown/createMdKitTiptapExtensions.ts +42 -0
  124. package/src/markdown/editorDebug.ts +5 -0
  125. package/src/markdown/markdownFenceRanges.ts +68 -0
  126. package/src/markdown/normalizeMarkdownSerialization.ts +55 -0
  127. package/src/markdown/prepareMarkdownForEditorHydration.ts +23 -0
  128. package/src/markdown/preserveMarkdownWhitespace.ts +143 -0
  129. package/src/markdown/yamlFrontMatter.ts +135 -0
  130. package/src/server.ts +6 -0
  131. package/src/styles.css +125 -53
  132. package/src/theme/MdKitThemeEditor.tsx +134 -0
  133. package/src/theme/editorTheme.ts +72 -0
  134. package/src/transport/backend.ts +220 -0
  135. package/src/transport/fastify.ts +57 -0
  136. package/src/transport/http.ts +126 -0
  137. package/src/transport/index.ts +12 -0
  138. package/src/transport/rest.ts +80 -0
  139. package/src/transport/store.ts +45 -0
  140. package/src/transport/trpcClient.ts +90 -0
  141. package/src/transport/trpcServer.ts +66 -0
  142. package/src/trpc/client.ts +11 -0
  143. package/src/trpc/server.ts +12 -0
  144. package/src/trpc.ts +11 -0
  145. package/src/ui/joinClassNames.ts +3 -0
  146. package/src/versioning/VersionHistoryPanel.tsx +146 -0
  147. package/src/versioning/useMdKitDocumentVersions.ts +146 -0
  148. package/src/yjs/MdKitMarkdownYjs.ts +111 -0
  149. package/src/yjs/index.ts +8 -0
  150. package/docs/.vitepress/config.ts +0 -47
  151. package/docs/api.md +0 -512
  152. package/docs/architecture.md +0 -96
  153. package/docs/collaboration-persistence.md +0 -147
  154. package/docs/index.md +0 -341
  155. package/docs/permissions.md +0 -139
  156. package/docs/plain-text.md +0 -131
  157. package/docs/rest.md +0 -98
  158. package/docs/shadcn.md +0 -125
  159. package/docs/styling.md +0 -373
  160. package/docs/use-cases.md +0 -148
@@ -0,0 +1,271 @@
1
+ import type { MouseEvent, PointerEvent, ReactNode } from "react";
2
+ import { useEditorState, type Editor } from "@tiptap/react";
3
+ import { BubbleMenu, type BubbleMenuProps } from "@tiptap/react/menus";
4
+ import {
5
+ Bold,
6
+ Code2,
7
+ Heading1,
8
+ Heading2,
9
+ Heading3,
10
+ Italic,
11
+ Link2,
12
+ List,
13
+ ListOrdered,
14
+ Quote,
15
+ Strikethrough,
16
+ } from "lucide-react";
17
+ import { joinClassNames } from "../ui/joinClassNames";
18
+
19
+ type MarkdownBubbleMenuProps = {
20
+ editor: Editor;
21
+ };
22
+
23
+ type BubbleMenuShouldShowProps = Parameters<
24
+ NonNullable<BubbleMenuProps["shouldShow"]>
25
+ >[0];
26
+
27
+ type ToolbarActiveState = {
28
+ blockquote: boolean;
29
+ bold: boolean;
30
+ bulletList: boolean;
31
+ codeBlock: boolean;
32
+ heading1: boolean;
33
+ heading2: boolean;
34
+ heading3: boolean;
35
+ italic: boolean;
36
+ link: boolean;
37
+ orderedList: boolean;
38
+ strike: boolean;
39
+ };
40
+
41
+ const toolbarActiveStateIsEqual = (
42
+ left: ToolbarActiveState,
43
+ right: ToolbarActiveState | null,
44
+ ) => {
45
+ if (!right) {
46
+ return false;
47
+ }
48
+
49
+ return (
50
+ left.blockquote === right.blockquote &&
51
+ left.bold === right.bold &&
52
+ left.bulletList === right.bulletList &&
53
+ left.codeBlock === right.codeBlock &&
54
+ left.heading1 === right.heading1 &&
55
+ left.heading2 === right.heading2 &&
56
+ left.heading3 === right.heading3 &&
57
+ left.italic === right.italic &&
58
+ left.link === right.link &&
59
+ left.orderedList === right.orderedList &&
60
+ left.strike === right.strike
61
+ );
62
+ };
63
+
64
+ const useToolbarActiveState = (editor: Editor) =>
65
+ useEditorState({
66
+ editor,
67
+ selector: ({ editor: currentEditor }) => ({
68
+ blockquote: currentEditor.isActive("blockquote"),
69
+ bold: currentEditor.isActive("bold"),
70
+ bulletList: currentEditor.isActive("bulletList"),
71
+ codeBlock: currentEditor.isActive("codeBlock"),
72
+ heading1: currentEditor.isActive("heading", { level: 1 }),
73
+ heading2: currentEditor.isActive("heading", { level: 2 }),
74
+ heading3: currentEditor.isActive("heading", { level: 3 }),
75
+ italic: currentEditor.isActive("italic"),
76
+ link: currentEditor.isActive("link"),
77
+ orderedList: currentEditor.isActive("orderedList"),
78
+ strike: currentEditor.isActive("strike"),
79
+ }),
80
+ equalityFn: toolbarActiveStateIsEqual,
81
+ });
82
+
83
+ export const shouldShowMarkdownBubbleMenu = ({
84
+ editor,
85
+ element,
86
+ from,
87
+ state,
88
+ to,
89
+ view,
90
+ }: BubbleMenuShouldShowProps) => {
91
+ const editorHasFocus =
92
+ view.hasFocus() || element.contains(document.activeElement);
93
+
94
+ const selectedText = state.doc.textBetween(from, to);
95
+
96
+ return (
97
+ editor.isEditable &&
98
+ editorHasFocus &&
99
+ !state.selection.empty &&
100
+ selectedText.length > 0
101
+ );
102
+ };
103
+
104
+ const setLink = (editor: Editor) => {
105
+ const previousUrl = editor.getAttributes("link").href;
106
+ const nextUrl = window.prompt("URL", previousUrl);
107
+
108
+ if (nextUrl === null) {
109
+ return;
110
+ }
111
+
112
+ if (nextUrl === "") {
113
+ editor.chain().focus().extendMarkRange("link").unsetLink().run();
114
+ return;
115
+ }
116
+
117
+ editor
118
+ .chain()
119
+ .focus()
120
+ .extendMarkRange("link")
121
+ .setLink({ href: nextUrl })
122
+ .run();
123
+ };
124
+
125
+ const ToolbarButton = ({
126
+ ariaLabel,
127
+ children,
128
+ isActive,
129
+ onAction,
130
+ }: {
131
+ ariaLabel: string;
132
+ children: ReactNode;
133
+ isActive: boolean;
134
+ onAction: () => void;
135
+ }) => {
136
+ const runPointerAction = (event: PointerEvent<HTMLButtonElement>) => {
137
+ if (event.pointerType === "mouse" && event.button !== 0) {
138
+ return;
139
+ }
140
+
141
+ event.preventDefault();
142
+ event.stopPropagation();
143
+ onAction();
144
+ };
145
+
146
+ const runKeyboardAction = (event: MouseEvent<HTMLButtonElement>) => {
147
+ if (event.detail !== 0) {
148
+ return;
149
+ }
150
+
151
+ onAction();
152
+ };
153
+
154
+ return (
155
+ <button
156
+ type="button"
157
+ className={joinClassNames(
158
+ "mp-lb-mdkit-toolbar-button",
159
+ isActive && "mp-lb-mdkit-toolbar-button-active",
160
+ )}
161
+ aria-label={ariaLabel}
162
+ aria-pressed={isActive}
163
+ onClick={runKeyboardAction}
164
+ onPointerDown={runPointerAction}
165
+ >
166
+ {children}
167
+ </button>
168
+ );
169
+ };
170
+
171
+ export const MarkdownBubbleMenu = ({ editor }: MarkdownBubbleMenuProps) => {
172
+ const activeState = useToolbarActiveState(editor);
173
+
174
+ return (
175
+ <BubbleMenu
176
+ className="mp-lb-mdkit-toolbar"
177
+ editor={editor}
178
+ options={{
179
+ placement: "top",
180
+ }}
181
+ shouldShow={shouldShowMarkdownBubbleMenu}
182
+ >
183
+ <ToolbarButton
184
+ ariaLabel="Bold"
185
+ isActive={activeState.bold}
186
+ onAction={() => editor.chain().focus().toggleBold().run()}
187
+ >
188
+ <Bold />
189
+ </ToolbarButton>
190
+ <ToolbarButton
191
+ ariaLabel="Italic"
192
+ isActive={activeState.italic}
193
+ onAction={() => editor.chain().focus().toggleItalic().run()}
194
+ >
195
+ <Italic />
196
+ </ToolbarButton>
197
+ <ToolbarButton
198
+ ariaLabel="Strikethrough"
199
+ isActive={activeState.strike}
200
+ onAction={() => editor.chain().focus().toggleStrike().run()}
201
+ >
202
+ <Strikethrough />
203
+ </ToolbarButton>
204
+ <ToolbarButton
205
+ ariaLabel="Code block"
206
+ isActive={activeState.codeBlock}
207
+ onAction={() => editor.chain().focus().toggleCodeBlock().run()}
208
+ >
209
+ <Code2 />
210
+ </ToolbarButton>
211
+ <div className="mp-lb-mdkit-toolbar-divider" />
212
+ <ToolbarButton
213
+ ariaLabel="Heading 1"
214
+ isActive={activeState.heading1}
215
+ onAction={() =>
216
+ editor.chain().focus().toggleHeading({ level: 1 }).run()
217
+ }
218
+ >
219
+ <Heading1 />
220
+ </ToolbarButton>
221
+ <ToolbarButton
222
+ ariaLabel="Heading 2"
223
+ isActive={activeState.heading2}
224
+ onAction={() =>
225
+ editor.chain().focus().toggleHeading({ level: 2 }).run()
226
+ }
227
+ >
228
+ <Heading2 />
229
+ </ToolbarButton>
230
+ <ToolbarButton
231
+ ariaLabel="Heading 3"
232
+ isActive={activeState.heading3}
233
+ onAction={() =>
234
+ editor.chain().focus().toggleHeading({ level: 3 }).run()
235
+ }
236
+ >
237
+ <Heading3 />
238
+ </ToolbarButton>
239
+ <div className="mp-lb-mdkit-toolbar-divider" />
240
+ <ToolbarButton
241
+ ariaLabel="Bullet list"
242
+ isActive={activeState.bulletList}
243
+ onAction={() => editor.chain().focus().toggleBulletList().run()}
244
+ >
245
+ <List />
246
+ </ToolbarButton>
247
+ <ToolbarButton
248
+ ariaLabel="Ordered list"
249
+ isActive={activeState.orderedList}
250
+ onAction={() => editor.chain().focus().toggleOrderedList().run()}
251
+ >
252
+ <ListOrdered />
253
+ </ToolbarButton>
254
+ <ToolbarButton
255
+ ariaLabel="Blockquote"
256
+ isActive={activeState.blockquote}
257
+ onAction={() => editor.chain().focus().toggleBlockquote().run()}
258
+ >
259
+ <Quote />
260
+ </ToolbarButton>
261
+ <div className="mp-lb-mdkit-toolbar-divider" />
262
+ <ToolbarButton
263
+ ariaLabel="Link"
264
+ isActive={activeState.link}
265
+ onAction={() => setLink(editor)}
266
+ >
267
+ <Link2 />
268
+ </ToolbarButton>
269
+ </BubbleMenu>
270
+ );
271
+ };
@@ -0,0 +1,81 @@
1
+ import { Extension } from "@tiptap/core";
2
+ import { Plugin, PluginKey } from "@tiptap/pm/state";
3
+
4
+ const markdownPastePluginKey = new PluginKey("mdkitMarkdownPaste");
5
+
6
+ const blockMarkdownPatterns = [
7
+ /^ {0,3}#{1,6}\s+\S/m,
8
+ /^ {0,3}(?:[-+*]|\d+[.)])\s+\S/m,
9
+ /^ {0,3}>\s+\S/m,
10
+ /^ {0,3}(?:```|~~~)/m,
11
+ /^ {0,3}(?:-{3,}|\*{3,}|_{3,})\s*$/m,
12
+ /^\|.+\|\s*\n\|(?:\s*:?-{3,}:?\s*\|)+/m,
13
+ ];
14
+
15
+ const inlineMarkdownPatterns = [
16
+ /!\[[^\]]*]\([^)]+\)/,
17
+ /\[[^\]]+]\([^)]+\)/,
18
+ /(^|[^\w])(?:\*\*|__)\S[\s\S]*?\S(?:\*\*|__)($|[^\w])/,
19
+ /(^|[^\w])`[^`\n]+`($|[^\w])/,
20
+ ];
21
+
22
+ const getClipboardValue = (
23
+ event: ClipboardEvent,
24
+ mimeType: string,
25
+ ): string => {
26
+ try {
27
+ return event.clipboardData?.getData(mimeType) ?? "";
28
+ } catch {
29
+ return "";
30
+ }
31
+ };
32
+
33
+ export const shouldPastePlainTextAsMarkdown = (text: string) => {
34
+ const trimmedText = text.trim();
35
+
36
+ if (trimmedText.length === 0) {
37
+ return false;
38
+ }
39
+
40
+ return [...blockMarkdownPatterns, ...inlineMarkdownPatterns].some((pattern) =>
41
+ pattern.test(trimmedText),
42
+ );
43
+ };
44
+
45
+ export const MarkdownPasteExtension = Extension.create({
46
+ name: "mdkitMarkdownPaste",
47
+
48
+ addProseMirrorPlugins() {
49
+ return [
50
+ new Plugin({
51
+ key: markdownPastePluginKey,
52
+ props: {
53
+ handlePaste: (view, event) => {
54
+ if (view.state.selection.$from.parent.type.spec.code) {
55
+ return false;
56
+ }
57
+
58
+ const markdownText = getClipboardValue(event, "text/markdown");
59
+ const plainText = getClipboardValue(event, "text/plain");
60
+ const pastedMarkdown = markdownText || plainText;
61
+
62
+ if (!pastedMarkdown.trim()) {
63
+ return false;
64
+ }
65
+
66
+ if (
67
+ !markdownText &&
68
+ !shouldPastePlainTextAsMarkdown(pastedMarkdown)
69
+ ) {
70
+ return false;
71
+ }
72
+
73
+ return this.editor.commands.insertContent(pastedMarkdown, {
74
+ contentType: "markdown",
75
+ });
76
+ },
77
+ },
78
+ }),
79
+ ];
80
+ },
81
+ });
@@ -0,0 +1,77 @@
1
+ import { Extension } from "@tiptap/core";
2
+ import type { Node } from "@tiptap/pm/model";
3
+ import { Plugin, PluginKey } from "@tiptap/pm/state";
4
+ import { Decoration, DecorationSet } from "@tiptap/pm/view";
5
+
6
+ export type MarkdownSearchMatch = {
7
+ from: number;
8
+ to: number;
9
+ };
10
+
11
+ type MarkdownSearchMeta = {
12
+ activeIndex: number;
13
+ matches: MarkdownSearchMatch[];
14
+ };
15
+
16
+ export const markdownSearchPluginKey = new PluginKey<DecorationSet>(
17
+ "mdkitMarkdownSearch",
18
+ );
19
+
20
+ const createSearchDecorations = (
21
+ document: Node,
22
+ { activeIndex, matches }: MarkdownSearchMeta,
23
+ ) =>
24
+ DecorationSet.create(
25
+ document,
26
+ matches.map((match, index) =>
27
+ Decoration.inline(match.from, match.to, {
28
+ class:
29
+ index === activeIndex
30
+ ? "mp-lb-mdkit-search-match mp-lb-mdkit-search-match-active"
31
+ : "mp-lb-mdkit-search-match",
32
+ }),
33
+ ),
34
+ );
35
+
36
+ export const MarkdownSearchExtension = Extension.create({
37
+ name: "mdkitMarkdownSearch",
38
+
39
+ addProseMirrorPlugins() {
40
+ return [
41
+ new Plugin<DecorationSet>({
42
+ key: markdownSearchPluginKey,
43
+ props: {
44
+ decorations(state) {
45
+ return markdownSearchPluginKey.getState(state);
46
+ },
47
+ },
48
+ state: {
49
+ apply(transaction, previousDecorations) {
50
+ const searchMeta = transaction.getMeta(markdownSearchPluginKey) as
51
+ | MarkdownSearchMeta
52
+ | undefined;
53
+
54
+ if (searchMeta) {
55
+ return createSearchDecorations(transaction.doc, searchMeta);
56
+ }
57
+
58
+ if (transaction.docChanged) {
59
+ return previousDecorations.map(
60
+ transaction.mapping,
61
+ transaction.doc,
62
+ );
63
+ }
64
+
65
+ return previousDecorations;
66
+ },
67
+ init(_config, instance) {
68
+ return createSearchDecorations(instance.doc, {
69
+ activeIndex: 0,
70
+ matches: [],
71
+ });
72
+ },
73
+ },
74
+ }),
75
+ ];
76
+ },
77
+ });
@@ -0,0 +1,98 @@
1
+ import { ChevronDown, ChevronUp, Search, X } from "lucide-react";
2
+ import type { FormEvent, KeyboardEvent, RefObject } from "react";
3
+
4
+ type MarkdownSearchPanelProps = {
5
+ activeMatchNumber: number;
6
+ inputRef: RefObject<HTMLInputElement | null>;
7
+ matchCount: number;
8
+ onClose: () => void;
9
+ onPrevious: () => void;
10
+ onNext: () => void;
11
+ onQueryChange: (query: string) => void;
12
+ query: string;
13
+ };
14
+
15
+ export const MarkdownSearchPanel = ({
16
+ activeMatchNumber,
17
+ inputRef,
18
+ matchCount,
19
+ onClose,
20
+ onPrevious,
21
+ onNext,
22
+ onQueryChange,
23
+ query,
24
+ }: MarkdownSearchPanelProps) => {
25
+ const submitSearch = (event: FormEvent<HTMLFormElement>) => {
26
+ event.preventDefault();
27
+ onNext();
28
+ };
29
+
30
+ const handleSearchKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {
31
+ if (event.key === "Escape") {
32
+ event.preventDefault();
33
+ onClose();
34
+ return;
35
+ }
36
+
37
+ if (event.key === "Enter" && event.shiftKey) {
38
+ event.preventDefault();
39
+ onPrevious();
40
+ }
41
+ };
42
+
43
+ const matchStatus =
44
+ query.trim().length === 0
45
+ ? "No query"
46
+ : matchCount === 0
47
+ ? "No matches"
48
+ : `${activeMatchNumber} of ${matchCount}`;
49
+
50
+ return (
51
+ <form
52
+ aria-label="Search document"
53
+ className="mp-lb-mdkit-search-panel"
54
+ onSubmit={submitSearch}
55
+ >
56
+ <Search aria-hidden="true" className="mp-lb-mdkit-search-icon" />
57
+ <input
58
+ ref={inputRef}
59
+ aria-label="Search document"
60
+ className="mp-lb-mdkit-search-input"
61
+ onChange={(event) => onQueryChange(event.target.value)}
62
+ onKeyDown={handleSearchKeyDown}
63
+ placeholder="Search"
64
+ type="search"
65
+ value={query}
66
+ />
67
+ <span className="mp-lb-mdkit-search-status">{matchStatus}</span>
68
+ <button
69
+ aria-label="Previous match"
70
+ className="mp-lb-mdkit-search-button"
71
+ disabled={matchCount === 0}
72
+ onClick={onPrevious}
73
+ title="Previous match"
74
+ type="button"
75
+ >
76
+ <ChevronUp aria-hidden="true" />
77
+ </button>
78
+ <button
79
+ aria-label="Next match"
80
+ className="mp-lb-mdkit-search-button"
81
+ disabled={matchCount === 0}
82
+ title="Next match"
83
+ type="submit"
84
+ >
85
+ <ChevronDown aria-hidden="true" />
86
+ </button>
87
+ <button
88
+ aria-label="Close search"
89
+ className="mp-lb-mdkit-search-button"
90
+ onClick={onClose}
91
+ title="Close search"
92
+ type="button"
93
+ >
94
+ <X aria-hidden="true" />
95
+ </button>
96
+ </form>
97
+ );
98
+ };
@@ -0,0 +1,75 @@
1
+ import type { CSSProperties } from "react";
2
+ import type { MdKitCollaborationSession } from "../document/documentTypes";
3
+ import { joinClassNames } from "../ui/joinClassNames";
4
+ import type { MdKitEditorDebugEvent } from "./editorDebug";
5
+ import { TiptapMarkdownSurface } from "./TiptapMarkdownSurface";
6
+
7
+ type MdKitEditorBaseProps = {
8
+ className?: string;
9
+ documentMargins?: boolean;
10
+ fillHeight?: boolean;
11
+ fixedWidth?: boolean;
12
+ ignoreYamlFrontMatter?: boolean;
13
+ instanceKey?: string | number;
14
+ onDebugEvent?: (event: MdKitEditorDebugEvent) => void;
15
+ onFocusChange?: (focused: boolean) => void;
16
+ readOnly?: boolean;
17
+ search?: boolean;
18
+ style?: CSSProperties;
19
+ };
20
+
21
+ type LocalMdKitEditorProps = MdKitEditorBaseProps & {
22
+ collaboration?: null;
23
+ onChange?: (markdown: string) => void;
24
+ value: string;
25
+ };
26
+
27
+ type CollaborativeMdKitEditorProps = MdKitEditorBaseProps & {
28
+ collaboration: MdKitCollaborationSession;
29
+ onChange?: (markdown: string) => void;
30
+ value?: string;
31
+ };
32
+
33
+ export type MdKitEditorProps =
34
+ | CollaborativeMdKitEditorProps
35
+ | LocalMdKitEditorProps;
36
+
37
+ /**
38
+ * Markdown-first rich text editor. Behaves like a controlled textarea in local
39
+ * mode and switches to a Yjs-backed engine when given a collaboration session.
40
+ *
41
+ * @remarks
42
+ * In collaborative mode the Yjs document is the content source; external
43
+ * `value` changes are not applied into the shared document.
44
+ */
45
+ export const MdKitEditor = (props: MdKitEditorProps) => {
46
+ const {
47
+ className,
48
+ documentMargins = false,
49
+ fillHeight = false,
50
+ fixedWidth = false,
51
+ readOnly = false,
52
+ style,
53
+ ...surfaceProps
54
+ } = props;
55
+
56
+ return (
57
+ <div
58
+ className={joinClassNames(
59
+ "mp-lb-mdkit-markdown-editor",
60
+ documentMargins && "mp-lb-mdkit-markdown-editor-document-margins",
61
+ fillHeight && "mp-lb-mdkit-markdown-editor-fill-height",
62
+ fixedWidth && "mp-lb-mdkit-markdown-editor-fixed-width",
63
+ className,
64
+ )}
65
+ data-read-only={readOnly ? "true" : undefined}
66
+ style={style}
67
+ >
68
+ <TiptapMarkdownSurface
69
+ key={props.instanceKey}
70
+ readOnly={readOnly}
71
+ {...surfaceProps}
72
+ />
73
+ </div>
74
+ );
75
+ };
@@ -0,0 +1,80 @@
1
+ import type { CSSProperties } from "react";
2
+ import ReactMarkdown from "react-markdown";
3
+ import remarkGfm from "remark-gfm";
4
+ import { joinClassNames } from "../ui/joinClassNames";
5
+ import { removeYamlFrontMatter } from "./yamlFrontMatter";
6
+
7
+ export type MdKitViewProps = {
8
+ className?: string;
9
+ documentMargins?: boolean;
10
+ fillHeight?: boolean;
11
+ fixedWidth?: boolean;
12
+ ignoreYamlFrontMatter?: boolean;
13
+ placeholder?: string;
14
+ style?: CSSProperties;
15
+ value: string;
16
+ };
17
+
18
+ /**
19
+ * Read-only markdown surface that mirrors {@link MdKitEditor}'s styling and
20
+ * sizing contract without mounting the editor runtime. Use it for previews and
21
+ * restored checkpoints.
22
+ */
23
+ export const MdKitView = ({
24
+ className,
25
+ documentMargins = false,
26
+ fillHeight = false,
27
+ fixedWidth = false,
28
+ ignoreYamlFrontMatter = false,
29
+ placeholder,
30
+ style,
31
+ value,
32
+ }: MdKitViewProps) => {
33
+ const markdownValue = ignoreYamlFrontMatter
34
+ ? removeYamlFrontMatter(value)
35
+ : value;
36
+ const renderedValue =
37
+ markdownValue.trim().length > 0 ? markdownValue : (placeholder ?? "");
38
+
39
+ return (
40
+ <div
41
+ className={joinClassNames(
42
+ "mp-lb-mdkit-markdown-editor",
43
+ "mp-lb-mdkit-markdown-view",
44
+ documentMargins && "mp-lb-mdkit-markdown-editor-document-margins",
45
+ fillHeight && "mp-lb-mdkit-markdown-editor-fill-height",
46
+ fixedWidth && "mp-lb-mdkit-markdown-editor-fixed-width",
47
+ className,
48
+ )}
49
+ data-read-only="true"
50
+ style={style}
51
+ >
52
+ <div className="mp-lb-mdkit-editor-shell">
53
+ <div className="mp-lb-mdkit-editor-surface">
54
+ {renderedValue.length > 0 ? (
55
+ <div className="mp-lb-mdkit-tiptap mp-lb-mdkit-view-content">
56
+ <ReactMarkdown
57
+ components={{
58
+ a: ({ children, ...linkProps }) => (
59
+ <a
60
+ {...linkProps}
61
+ rel="noopener noreferrer"
62
+ target="_blank"
63
+ >
64
+ {children}
65
+ </a>
66
+ ),
67
+ }}
68
+ remarkPlugins={[remarkGfm]}
69
+ >
70
+ {renderedValue}
71
+ </ReactMarkdown>
72
+ </div>
73
+ ) : (
74
+ <div className="mp-lb-mdkit-editor-empty" />
75
+ )}
76
+ </div>
77
+ </div>
78
+ </div>
79
+ );
80
+ };