@mp-lb/mdkit 0.2.5-main.27.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -6,6 +6,7 @@ export { MdKitConflictPanel } from "./document/MdKitConflictPanel.js";
6
6
  export { MdKitDocumentToolbar } from "./document/MdKitDocumentToolbar.js";
7
7
  export { MdKitEditor } from "./markdown/MdKitEditor.js";
8
8
  export { MdKitView } from "./markdown/MdKitView.js";
9
+ export { extractYamlFrontMatter, hasYamlFrontMatter, parseYamlFrontMatter, prependYamlFrontMatter, removeYamlFrontMatter, } from "./markdown/yamlFrontMatter.js";
9
10
  export { MdKitThemeEditor } from "./theme/MdKitThemeEditor.js";
10
11
  export { createMdKitRestAdapter } from "./transport/rest.js";
11
12
  export { createMdKitEditorThemeStyle, darkMdKitEditorTheme, defaultMdKitEditorTheme, } from "./theme/editorTheme.js";
@@ -21,6 +22,7 @@ export type { MdKitDocumentToolbarProps } from "./document/MdKitDocumentToolbar.
21
22
  export type { MdKitEditorProps } from "./markdown/MdKitEditor.js";
22
23
  export type { MdKitEditorDebugEvent } from "./markdown/editorDebug.js";
23
24
  export type { MdKitViewProps } from "./markdown/MdKitView.js";
25
+ export type { MdKitYamlFrontMatter, MdKitYamlFrontMatterExtraction, } from "./markdown/yamlFrontMatter.js";
24
26
  export type { MdKitThemeEditorProps } from "./theme/MdKitThemeEditor.js";
25
27
  export type { CreateMdKitRestAdapterOptions } from "./transport/rest.js";
26
28
  export type { MdKitEditorTheme, MdKitEditorThemeStyle, } from "./theme/editorTheme.js";
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ export { MdKitConflictPanel } from "./document/MdKitConflictPanel.js";
6
6
  export { MdKitDocumentToolbar } from "./document/MdKitDocumentToolbar.js";
7
7
  export { MdKitEditor } from "./markdown/MdKitEditor.js";
8
8
  export { MdKitView } from "./markdown/MdKitView.js";
9
+ export { extractYamlFrontMatter, hasYamlFrontMatter, parseYamlFrontMatter, prependYamlFrontMatter, removeYamlFrontMatter, } from "./markdown/yamlFrontMatter.js";
9
10
  export { MdKitThemeEditor } from "./theme/MdKitThemeEditor.js";
10
11
  export { createMdKitRestAdapter } from "./transport/rest.js";
11
12
  export { createMdKitEditorThemeStyle, darkMdKitEditorTheme, defaultMdKitEditorTheme, } from "./theme/editorTheme.js";
@@ -0,0 +1,3 @@
1
+ import { Extension } from "@tiptap/core";
2
+ export declare const shouldPastePlainTextAsMarkdown: (text: string) => boolean;
3
+ export declare const MarkdownPasteExtension: Extension<any, any>;
@@ -0,0 +1,62 @@
1
+ import { Extension } from "@tiptap/core";
2
+ import { Plugin, PluginKey } from "@tiptap/pm/state";
3
+ const markdownPastePluginKey = new PluginKey("mdkitMarkdownPaste");
4
+ const blockMarkdownPatterns = [
5
+ /^ {0,3}#{1,6}\s+\S/m,
6
+ /^ {0,3}(?:[-+*]|\d+[.)])\s+\S/m,
7
+ /^ {0,3}>\s+\S/m,
8
+ /^ {0,3}(?:```|~~~)/m,
9
+ /^ {0,3}(?:-{3,}|\*{3,}|_{3,})\s*$/m,
10
+ /^\|.+\|\s*\n\|(?:\s*:?-{3,}:?\s*\|)+/m,
11
+ ];
12
+ const inlineMarkdownPatterns = [
13
+ /!\[[^\]]*]\([^)]+\)/,
14
+ /\[[^\]]+]\([^)]+\)/,
15
+ /(^|[^\w])(?:\*\*|__)\S[\s\S]*?\S(?:\*\*|__)($|[^\w])/,
16
+ /(^|[^\w])`[^`\n]+`($|[^\w])/,
17
+ ];
18
+ const getClipboardValue = (event, mimeType) => {
19
+ try {
20
+ return event.clipboardData?.getData(mimeType) ?? "";
21
+ }
22
+ catch {
23
+ return "";
24
+ }
25
+ };
26
+ export const shouldPastePlainTextAsMarkdown = (text) => {
27
+ const trimmedText = text.trim();
28
+ if (trimmedText.length === 0) {
29
+ return false;
30
+ }
31
+ return [...blockMarkdownPatterns, ...inlineMarkdownPatterns].some((pattern) => pattern.test(trimmedText));
32
+ };
33
+ export const MarkdownPasteExtension = Extension.create({
34
+ name: "mdkitMarkdownPaste",
35
+ addProseMirrorPlugins() {
36
+ return [
37
+ new Plugin({
38
+ key: markdownPastePluginKey,
39
+ props: {
40
+ handlePaste: (view, event) => {
41
+ if (view.state.selection.$from.parent.type.spec.code) {
42
+ return false;
43
+ }
44
+ const markdownText = getClipboardValue(event, "text/markdown");
45
+ const plainText = getClipboardValue(event, "text/plain");
46
+ const pastedMarkdown = markdownText || plainText;
47
+ if (!pastedMarkdown.trim()) {
48
+ return false;
49
+ }
50
+ if (!markdownText &&
51
+ !shouldPastePlainTextAsMarkdown(pastedMarkdown)) {
52
+ return false;
53
+ }
54
+ return this.editor.commands.insertContent(pastedMarkdown, {
55
+ contentType: "markdown",
56
+ });
57
+ },
58
+ },
59
+ }),
60
+ ];
61
+ },
62
+ });
@@ -0,0 +1,9 @@
1
+ import { Extension } from "@tiptap/core";
2
+ import { PluginKey } from "@tiptap/pm/state";
3
+ import { DecorationSet } from "@tiptap/pm/view";
4
+ export type MarkdownSearchMatch = {
5
+ from: number;
6
+ to: number;
7
+ };
8
+ export declare const markdownSearchPluginKey: PluginKey<DecorationSet>;
9
+ export declare const MarkdownSearchExtension: Extension<any, any>;
@@ -0,0 +1,42 @@
1
+ import { Extension } from "@tiptap/core";
2
+ import { Plugin, PluginKey } from "@tiptap/pm/state";
3
+ import { Decoration, DecorationSet } from "@tiptap/pm/view";
4
+ export const markdownSearchPluginKey = new PluginKey("mdkitMarkdownSearch");
5
+ const createSearchDecorations = (document, { activeIndex, matches }) => DecorationSet.create(document, matches.map((match, index) => Decoration.inline(match.from, match.to, {
6
+ class: index === activeIndex
7
+ ? "mp-lb-mdkit-search-match mp-lb-mdkit-search-match-active"
8
+ : "mp-lb-mdkit-search-match",
9
+ })));
10
+ export const MarkdownSearchExtension = Extension.create({
11
+ name: "mdkitMarkdownSearch",
12
+ addProseMirrorPlugins() {
13
+ return [
14
+ new Plugin({
15
+ key: markdownSearchPluginKey,
16
+ props: {
17
+ decorations(state) {
18
+ return markdownSearchPluginKey.getState(state);
19
+ },
20
+ },
21
+ state: {
22
+ apply(transaction, previousDecorations) {
23
+ const searchMeta = transaction.getMeta(markdownSearchPluginKey);
24
+ if (searchMeta) {
25
+ return createSearchDecorations(transaction.doc, searchMeta);
26
+ }
27
+ if (transaction.docChanged) {
28
+ return previousDecorations.map(transaction.mapping, transaction.doc);
29
+ }
30
+ return previousDecorations;
31
+ },
32
+ init(_config, instance) {
33
+ return createSearchDecorations(instance.doc, {
34
+ activeIndex: 0,
35
+ matches: [],
36
+ });
37
+ },
38
+ },
39
+ }),
40
+ ];
41
+ },
42
+ });
@@ -0,0 +1,13 @@
1
+ import type { RefObject } from "react";
2
+ type MarkdownSearchPanelProps = {
3
+ activeMatchNumber: number;
4
+ inputRef: RefObject<HTMLInputElement | null>;
5
+ matchCount: number;
6
+ onClose: () => void;
7
+ onPrevious: () => void;
8
+ onNext: () => void;
9
+ onQueryChange: (query: string) => void;
10
+ query: string;
11
+ };
12
+ export declare const MarkdownSearchPanel: ({ activeMatchNumber, inputRef, matchCount, onClose, onPrevious, onNext, onQueryChange, query, }: MarkdownSearchPanelProps) => import("react/jsx-runtime").JSX.Element;
13
+ export {};
@@ -0,0 +1,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { ChevronDown, ChevronUp, Search, X } from "lucide-react";
3
+ export const MarkdownSearchPanel = ({ activeMatchNumber, inputRef, matchCount, onClose, onPrevious, onNext, onQueryChange, query, }) => {
4
+ const submitSearch = (event) => {
5
+ event.preventDefault();
6
+ onNext();
7
+ };
8
+ const handleSearchKeyDown = (event) => {
9
+ if (event.key === "Escape") {
10
+ event.preventDefault();
11
+ onClose();
12
+ return;
13
+ }
14
+ if (event.key === "Enter" && event.shiftKey) {
15
+ event.preventDefault();
16
+ onPrevious();
17
+ }
18
+ };
19
+ const matchStatus = query.trim().length === 0
20
+ ? "No query"
21
+ : matchCount === 0
22
+ ? "No matches"
23
+ : `${activeMatchNumber} of ${matchCount}`;
24
+ return (_jsxs("form", { "aria-label": "Search document", className: "mp-lb-mdkit-search-panel", onSubmit: submitSearch, children: [_jsx(Search, { "aria-hidden": "true", className: "mp-lb-mdkit-search-icon" }), _jsx("input", { ref: inputRef, "aria-label": "Search document", className: "mp-lb-mdkit-search-input", onChange: (event) => onQueryChange(event.target.value), onKeyDown: handleSearchKeyDown, placeholder: "Search", type: "search", value: query }), _jsx("span", { className: "mp-lb-mdkit-search-status", children: matchStatus }), _jsx("button", { "aria-label": "Previous match", className: "mp-lb-mdkit-search-button", disabled: matchCount === 0, onClick: onPrevious, title: "Previous match", type: "button", children: _jsx(ChevronUp, { "aria-hidden": "true" }) }), _jsx("button", { "aria-label": "Next match", className: "mp-lb-mdkit-search-button", disabled: matchCount === 0, title: "Next match", type: "submit", children: _jsx(ChevronDown, { "aria-hidden": "true" }) }), _jsx("button", { "aria-label": "Close search", className: "mp-lb-mdkit-search-button", onClick: onClose, title: "Close search", type: "button", children: _jsx(X, { "aria-hidden": "true" }) })] }));
25
+ };
@@ -4,10 +4,12 @@ import type { MdKitEditorDebugEvent } from "./editorDebug.js";
4
4
  type MdKitEditorBaseProps = {
5
5
  className?: string;
6
6
  fillHeight?: boolean;
7
+ ignoreYamlFrontMatter?: boolean;
7
8
  instanceKey?: string | number;
8
9
  onDebugEvent?: (event: MdKitEditorDebugEvent) => void;
9
10
  onFocusChange?: (focused: boolean) => void;
10
11
  readOnly?: boolean;
12
+ search?: boolean;
11
13
  style?: CSSProperties;
12
14
  };
13
15
  type LocalMdKitEditorProps = MdKitEditorBaseProps & {
@@ -2,8 +2,9 @@ import type { CSSProperties } from "react";
2
2
  export type MdKitViewProps = {
3
3
  className?: string;
4
4
  fillHeight?: boolean;
5
+ ignoreYamlFrontMatter?: boolean;
5
6
  placeholder?: string;
6
7
  style?: CSSProperties;
7
8
  value: string;
8
9
  };
9
- export declare const MdKitView: ({ className, fillHeight, placeholder, style, value, }: MdKitViewProps) => import("react/jsx-runtime").JSX.Element;
10
+ export declare const MdKitView: ({ className, fillHeight, ignoreYamlFrontMatter, placeholder, style, value, }: MdKitViewProps) => import("react/jsx-runtime").JSX.Element;
@@ -2,8 +2,12 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import ReactMarkdown from "react-markdown";
3
3
  import remarkGfm from "remark-gfm";
4
4
  import { joinClassNames } from "../ui/joinClassNames.js";
5
- export const MdKitView = ({ className, fillHeight = false, placeholder, style, value, }) => {
6
- const renderedValue = value.trim().length > 0 ? value : (placeholder ?? "");
5
+ import { removeYamlFrontMatter } from "./yamlFrontMatter.js";
6
+ export const MdKitView = ({ className, fillHeight = false, ignoreYamlFrontMatter = false, placeholder, style, value, }) => {
7
+ const markdownValue = ignoreYamlFrontMatter
8
+ ? removeYamlFrontMatter(value)
9
+ : value;
10
+ const renderedValue = markdownValue.trim().length > 0 ? markdownValue : (placeholder ?? "");
7
11
  return (_jsx("div", { className: joinClassNames("mp-lb-mdkit-markdown-editor", "mp-lb-mdkit-markdown-view", fillHeight && "mp-lb-mdkit-markdown-editor-fill-height", className), "data-read-only": "true", style: style, children: _jsx("div", { className: "mp-lb-mdkit-editor-shell", children: _jsx("div", { className: "mp-lb-mdkit-editor-surface", children: renderedValue.length > 0 ? (_jsx("div", { className: "mp-lb-mdkit-tiptap mp-lb-mdkit-view-content", children: _jsx(ReactMarkdown, { components: {
8
12
  a: ({ children, ...linkProps }) => (_jsx("a", { ...linkProps, rel: "noopener noreferrer", target: "_blank", children: children })),
9
13
  }, remarkPlugins: [remarkGfm], children: renderedValue }) })) : (_jsx("div", { className: "mp-lb-mdkit-editor-empty" })) }) }) }));
@@ -5,8 +5,10 @@ type LocalTiptapMarkdownSurfaceProps = {
5
5
  onChange?: (markdown: string) => void;
6
6
  onDebugEvent?: (event: MdKitEditorDebugEvent) => void;
7
7
  onFocusChange?: (focused: boolean) => void;
8
+ ignoreYamlFrontMatter?: boolean;
8
9
  placeholder?: string;
9
10
  readOnly?: boolean;
11
+ search?: boolean;
10
12
  value: string;
11
13
  };
12
14
  type CollaborativeTiptapMarkdownSurfaceProps = {
@@ -14,8 +16,10 @@ type CollaborativeTiptapMarkdownSurfaceProps = {
14
16
  onChange?: (markdown: string) => void;
15
17
  onDebugEvent?: (event: MdKitEditorDebugEvent) => void;
16
18
  onFocusChange?: (focused: boolean) => void;
19
+ ignoreYamlFrontMatter?: boolean;
17
20
  placeholder?: string;
18
21
  readOnly?: boolean;
22
+ search?: boolean;
19
23
  value?: string;
20
24
  };
21
25
  type TiptapMarkdownSurfaceProps = CollaborativeTiptapMarkdownSurfaceProps | LocalTiptapMarkdownSurfaceProps;
@@ -1,10 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useEffect, useMemo, useRef } from "react";
2
+ import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
3
3
  import Collaboration from "@tiptap/extension-collaboration";
4
4
  import CollaborationCaret from "@tiptap/extension-collaboration-caret";
5
5
  import { EditorContent, useEditor } from "@tiptap/react";
6
6
  import { createMdKitTiptapExtensions } from "./createMdKitTiptapExtensions.js";
7
+ import { extractYamlFrontMatter, prependYamlFrontMatter, } from "./yamlFrontMatter.js";
7
8
  import { MarkdownBubbleMenu } from "./MarkdownBubbleMenu.js";
9
+ import { MarkdownSearchPanel } from "./MarkdownSearchPanel.js";
10
+ import { markdownSearchPluginKey, } from "./MarkdownSearchExtension.js";
8
11
  import { normalizeMarkdownSerialization } from "./normalizeMarkdownSerialization.js";
9
12
  import { prepareMarkdownForEditorHydration } from "./prepareMarkdownForEditorHydration.js";
10
13
  const describeElement = (element) => {
@@ -58,17 +61,24 @@ const createEditorDebugSnapshot = (editor, phase) => {
58
61
  };
59
62
  };
60
63
  export const TiptapMarkdownSurface = (props) => {
61
- const { collaboration = null, onDebugEvent, onFocusChange, placeholder = "Start writing...", readOnly = false, } = props;
64
+ const { collaboration = null, ignoreYamlFrontMatter = false, onDebugEvent, onFocusChange, placeholder = "Start writing...", readOnly = false, search = false, } = props;
62
65
  const markdownValue = "value" in props && typeof props.value === "string" ? props.value : "";
63
66
  const editorSurfaceRef = useRef(null);
67
+ const searchInputRef = useRef(null);
64
68
  const onDebugEventRef = useRef(onDebugEvent);
65
69
  const onFocusChangeRef = useRef(onFocusChange);
66
70
  const onChangeRef = useRef(props.onChange);
67
71
  const currentMarkdownRef = useRef(markdownValue);
72
+ const yamlFrontMatterRef = useRef(ignoreYamlFrontMatter
73
+ ? extractYamlFrontMatter(markdownValue).frontMatter
74
+ : null);
68
75
  const isApplyingExternalValueRef = useRef(false);
69
76
  const pendingControlledEchoesRef = useRef(new Set());
70
77
  const pendingContentFocusRef = useRef(null);
71
78
  const shouldFocusAfterPointerRef = useRef(false);
79
+ const [searchOpen, setSearchOpen] = useState(false);
80
+ const [searchQuery, setSearchQuery] = useState("");
81
+ const [activeSearchMatchIndex, setActiveSearchMatchIndex] = useState(0);
72
82
  const collaborationDocument = collaboration?.document ?? null;
73
83
  const collaborationProvider = collaboration?.provider ?? null;
74
84
  const collaborationUserColor = collaboration?.collaborator.color ?? "";
@@ -121,7 +131,9 @@ export const TiptapMarkdownSurface = (props) => {
121
131
  const editor = useEditor({
122
132
  content: hasCollaboration
123
133
  ? undefined
124
- : prepareMarkdownForEditorHydration(markdownValue),
134
+ : prepareMarkdownForEditorHydration(ignoreYamlFrontMatter
135
+ ? extractYamlFrontMatter(markdownValue).body
136
+ : markdownValue),
125
137
  contentType: "markdown",
126
138
  editable: !readOnly,
127
139
  editorProps: {
@@ -166,7 +178,7 @@ export const TiptapMarkdownSurface = (props) => {
166
178
  }
167
179
  const nextSerializedMarkdown = normalizeMarkdownSerialization(updatedEditor.getMarkdown());
168
180
  const previousMarkdown = currentMarkdownRef.current;
169
- const nextMarkdown = nextSerializedMarkdown;
181
+ const nextMarkdown = prependYamlFrontMatter(yamlFrontMatterRef.current, nextSerializedMarkdown);
170
182
  currentMarkdownRef.current = nextMarkdown;
171
183
  if (nextMarkdown !== previousMarkdown) {
172
184
  pendingControlledEchoesRef.current.add(nextMarkdown);
@@ -177,8 +189,150 @@ export const TiptapMarkdownSurface = (props) => {
177
189
  collaborationCaretExtensions,
178
190
  collaborationDocument,
179
191
  hasCollaboration,
192
+ ignoreYamlFrontMatter,
180
193
  placeholder,
181
194
  ]);
195
+ const searchMatches = useMemo(() => {
196
+ const query = searchQuery.trim().toLocaleLowerCase();
197
+ if (!editor || query.length === 0) {
198
+ return [];
199
+ }
200
+ const matches = [];
201
+ editor.state.doc.descendants((node, position) => {
202
+ if (!node.isText || typeof node.text !== "string") {
203
+ return;
204
+ }
205
+ const text = node.text.toLocaleLowerCase();
206
+ let fromIndex = text.indexOf(query);
207
+ while (fromIndex >= 0) {
208
+ matches.push({
209
+ from: position + fromIndex,
210
+ to: position + fromIndex + query.length,
211
+ });
212
+ fromIndex = text.indexOf(query, fromIndex + query.length);
213
+ }
214
+ });
215
+ return matches;
216
+ }, [editor, searchQuery, markdownValue]);
217
+ const activeSearchMatchNumber = searchMatches.length === 0 ? 0 : activeSearchMatchIndex + 1;
218
+ const scrollActiveSearchMatchIntoView = useCallback(() => {
219
+ window.requestAnimationFrame(() => {
220
+ const activeMatch = editorSurfaceRef.current?.querySelector(".mp-lb-mdkit-search-match-active");
221
+ if (!activeMatch || !("scrollIntoView" in activeMatch)) {
222
+ return;
223
+ }
224
+ activeMatch.scrollIntoView({
225
+ block: "center",
226
+ inline: "nearest",
227
+ });
228
+ });
229
+ }, []);
230
+ const selectSearchMatch = useCallback((matchIndex) => {
231
+ if (!editor || searchMatches.length === 0) {
232
+ return;
233
+ }
234
+ const nextIndex = ((matchIndex % searchMatches.length) + searchMatches.length) %
235
+ searchMatches.length;
236
+ setActiveSearchMatchIndex(nextIndex);
237
+ editor.view.dispatch(editor.state.tr
238
+ .setMeta(markdownSearchPluginKey, {
239
+ activeIndex: nextIndex,
240
+ matches: searchMatches,
241
+ })
242
+ .setMeta("addToHistory", false));
243
+ scrollActiveSearchMatchIntoView();
244
+ }, [editor, scrollActiveSearchMatchIntoView, searchMatches]);
245
+ const openSearch = useCallback(() => {
246
+ if (!search) {
247
+ return;
248
+ }
249
+ setSearchOpen(true);
250
+ window.requestAnimationFrame(() => {
251
+ searchInputRef.current?.focus();
252
+ searchInputRef.current?.select();
253
+ });
254
+ }, [search]);
255
+ const closeSearch = useCallback(() => {
256
+ setSearchOpen(false);
257
+ if (!editor) {
258
+ return;
259
+ }
260
+ const activeMatch = searchMatches[activeSearchMatchIndex];
261
+ if (!activeMatch) {
262
+ editor.commands.focus();
263
+ return;
264
+ }
265
+ editor
266
+ .chain()
267
+ .focus()
268
+ .setTextSelection({ from: activeMatch.from, to: activeMatch.to })
269
+ .scrollIntoView()
270
+ .run();
271
+ }, [activeSearchMatchIndex, editor, searchMatches]);
272
+ const selectNextSearchMatch = useCallback(() => {
273
+ selectSearchMatch(activeSearchMatchIndex + 1);
274
+ }, [activeSearchMatchIndex, selectSearchMatch]);
275
+ const selectPreviousSearchMatch = useCallback(() => {
276
+ selectSearchMatch(activeSearchMatchIndex - 1);
277
+ }, [activeSearchMatchIndex, selectSearchMatch]);
278
+ useEffect(() => {
279
+ if (!search) {
280
+ setSearchOpen(false);
281
+ setSearchQuery("");
282
+ }
283
+ }, [search]);
284
+ useEffect(() => {
285
+ if (!search || !editor) {
286
+ return;
287
+ }
288
+ const handleSearchShortcut = (event) => {
289
+ const isFindShortcut = (event.metaKey || event.ctrlKey) &&
290
+ !event.altKey &&
291
+ event.key.toLocaleLowerCase() === "f";
292
+ if (!isFindShortcut) {
293
+ return;
294
+ }
295
+ if (document.activeElement instanceof Element &&
296
+ !editorSurfaceRef.current?.contains(document.activeElement)) {
297
+ return;
298
+ }
299
+ event.preventDefault();
300
+ openSearch();
301
+ };
302
+ document.addEventListener("keydown", handleSearchShortcut);
303
+ return () => {
304
+ document.removeEventListener("keydown", handleSearchShortcut);
305
+ };
306
+ }, [editor, openSearch, search]);
307
+ useEffect(() => {
308
+ setActiveSearchMatchIndex(0);
309
+ }, [searchQuery]);
310
+ useEffect(() => {
311
+ if (!searchOpen || searchQuery.trim().length === 0) {
312
+ editor?.view.dispatch(editor.state.tr
313
+ .setMeta(markdownSearchPluginKey, {
314
+ activeIndex: 0,
315
+ matches: [],
316
+ })
317
+ .setMeta("addToHistory", false));
318
+ return;
319
+ }
320
+ const nextActiveSearchMatchIndex = Math.min(activeSearchMatchIndex, Math.max(0, searchMatches.length - 1));
321
+ editor?.view.dispatch(editor.state.tr
322
+ .setMeta(markdownSearchPluginKey, {
323
+ activeIndex: nextActiveSearchMatchIndex,
324
+ matches: searchMatches,
325
+ })
326
+ .setMeta("addToHistory", false));
327
+ scrollActiveSearchMatchIntoView();
328
+ }, [
329
+ activeSearchMatchIndex,
330
+ editor,
331
+ scrollActiveSearchMatchIntoView,
332
+ searchMatches,
333
+ searchOpen,
334
+ searchQuery,
335
+ ]);
182
336
  useEffect(() => {
183
337
  editor?.setEditable(!readOnly);
184
338
  }, [editor, readOnly]);
@@ -215,6 +369,9 @@ export const TiptapMarkdownSurface = (props) => {
215
369
  }
216
370
  if (hasCollaboration) {
217
371
  currentMarkdownRef.current = markdownValue;
372
+ yamlFrontMatterRef.current = ignoreYamlFrontMatter
373
+ ? extractYamlFrontMatter(markdownValue).frontMatter
374
+ : null;
218
375
  pendingControlledEchoesRef.current.clear();
219
376
  return;
220
377
  }
@@ -228,15 +385,19 @@ export const TiptapMarkdownSurface = (props) => {
228
385
  }
229
386
  pendingControlledEchoesRef.current.clear();
230
387
  isApplyingExternalValueRef.current = true;
231
- editor.commands.setContent(prepareMarkdownForEditorHydration(markdownValue), {
388
+ const frontMatter = ignoreYamlFrontMatter
389
+ ? extractYamlFrontMatter(markdownValue)
390
+ : null;
391
+ editor.commands.setContent(prepareMarkdownForEditorHydration(frontMatter?.body ?? markdownValue), {
232
392
  contentType: "markdown",
233
393
  emitUpdate: false,
234
394
  });
235
395
  currentMarkdownRef.current = markdownValue;
396
+ yamlFrontMatterRef.current = frontMatter?.frontMatter ?? null;
236
397
  window.queueMicrotask(() => {
237
398
  isApplyingExternalValueRef.current = false;
238
399
  });
239
- }, [editor, hasCollaboration, markdownValue]);
400
+ }, [editor, hasCollaboration, ignoreYamlFrontMatter, markdownValue]);
240
401
  if (!editor) {
241
402
  return (_jsx("div", { className: "mp-lb-mdkit-editor-shell", children: _jsx("div", { className: "mp-lb-mdkit-editor-empty", children: collaboration
242
403
  ? "Connecting collaboration session..."
@@ -449,5 +610,5 @@ export const TiptapMarkdownSurface = (props) => {
449
610
  });
450
611
  queueEditorFocusAtPosition(getEditorPositionAtClientPoint(event.clientX, event.clientY, event.target));
451
612
  };
452
- return (_jsx("div", { className: "mp-lb-mdkit-editor-shell", children: _jsxs("div", { ref: editorSurfaceRef, className: "mp-lb-mdkit-editor-surface", onPointerDownCapture: focusEditorBackgroundOnPointerDown, onPointerUpCapture: focusEditorBackgroundOnPointerUp, children: [_jsx(MarkdownBubbleMenu, { editor: editor }), _jsx(EditorContent, { editor: editor })] }) }));
613
+ return (_jsx("div", { className: "mp-lb-mdkit-editor-shell", children: _jsxs("div", { ref: editorSurfaceRef, className: "mp-lb-mdkit-editor-surface", onPointerDownCapture: focusEditorBackgroundOnPointerDown, onPointerUpCapture: focusEditorBackgroundOnPointerUp, children: [search && searchOpen ? (_jsx(MarkdownSearchPanel, { activeMatchNumber: activeSearchMatchNumber, inputRef: searchInputRef, matchCount: searchMatches.length, onClose: closeSearch, onNext: selectNextSearchMatch, onPrevious: selectPreviousSearchMatch, onQueryChange: setSearchQuery, query: searchQuery })) : null, _jsx(MarkdownBubbleMenu, { editor: editor }), _jsx(EditorContent, { editor: editor })] }) }));
453
614
  };
@@ -1,6 +1,8 @@
1
1
  import { Markdown } from "@tiptap/markdown";
2
2
  import Placeholder from "@tiptap/extension-placeholder";
3
3
  import StarterKit from "@tiptap/starter-kit";
4
+ import { MarkdownPasteExtension } from "./MarkdownPasteExtension.js";
5
+ import { MarkdownSearchExtension } from "./MarkdownSearchExtension.js";
4
6
  export const defaultMdKitMarkdownPlaceholder = "Start writing...";
5
7
  export const createMdKitTiptapExtensions = ({ placeholder = defaultMdKitMarkdownPlaceholder, undoRedo = true, } = {}) => [
6
8
  StarterKit.configure({
@@ -24,4 +26,6 @@ export const createMdKitTiptapExtensions = ({ placeholder = defaultMdKitMarkdown
24
26
  gfm: true,
25
27
  },
26
28
  }),
29
+ MarkdownPasteExtension,
30
+ MarkdownSearchExtension,
27
31
  ];
@@ -0,0 +1,16 @@
1
+ export type MdKitYamlFrontMatter = {
2
+ data: unknown;
3
+ raw: string;
4
+ trailingWhitespace: string;
5
+ yaml: string;
6
+ };
7
+ export type MdKitYamlFrontMatterExtraction = {
8
+ body: string;
9
+ errors: string[];
10
+ frontMatter: MdKitYamlFrontMatter | null;
11
+ };
12
+ export declare const parseYamlFrontMatter: (yaml: string) => unknown;
13
+ export declare const extractYamlFrontMatter: (markdown: string) => MdKitYamlFrontMatterExtraction;
14
+ export declare const hasYamlFrontMatter: (markdown: string) => boolean;
15
+ export declare const removeYamlFrontMatter: (markdown: string) => string;
16
+ export declare const prependYamlFrontMatter: (frontMatter: MdKitYamlFrontMatter | string | null, body: string) => string;
@@ -0,0 +1,88 @@
1
+ import { parseDocument } from "yaml";
2
+ const delimiter = "---";
3
+ const getLineEnd = (markdown, lineStart) => {
4
+ const newlineIndex = markdown.indexOf("\n", lineStart);
5
+ if (newlineIndex === -1) {
6
+ return {
7
+ contentEnd: markdown.length,
8
+ lineEnd: markdown.length,
9
+ newline: "",
10
+ };
11
+ }
12
+ const contentEnd = newlineIndex > lineStart && markdown[newlineIndex - 1] === "\r"
13
+ ? newlineIndex - 1
14
+ : newlineIndex;
15
+ return {
16
+ contentEnd,
17
+ lineEnd: newlineIndex + 1,
18
+ newline: markdown.slice(contentEnd, newlineIndex + 1),
19
+ };
20
+ };
21
+ const getNextBodyStart = (markdown, lineStart) => {
22
+ let bodyStart = lineStart;
23
+ while (bodyStart < markdown.length) {
24
+ const lineEnd = getLineEnd(markdown, bodyStart);
25
+ const line = markdown.slice(bodyStart, lineEnd.contentEnd);
26
+ if (!/^[ \t]*$/.test(line)) {
27
+ break;
28
+ }
29
+ bodyStart = lineEnd.lineEnd;
30
+ }
31
+ return bodyStart;
32
+ };
33
+ export const parseYamlFrontMatter = (yaml) => {
34
+ const document = parseDocument(yaml, { prettyErrors: false });
35
+ if (document.errors.length > 0) {
36
+ throw new Error(document.errors.map((error) => error.message).join("\n"));
37
+ }
38
+ return document.toJSON();
39
+ };
40
+ export const extractYamlFrontMatter = (markdown) => {
41
+ const openingLine = getLineEnd(markdown, 0);
42
+ if (markdown.slice(0, openingLine.contentEnd) !== delimiter) {
43
+ return { body: markdown, errors: [], frontMatter: null };
44
+ }
45
+ let lineStart = openingLine.lineEnd;
46
+ while (lineStart < markdown.length) {
47
+ const lineEnd = getLineEnd(markdown, lineStart);
48
+ const line = markdown.slice(lineStart, lineEnd.contentEnd);
49
+ if (line !== delimiter) {
50
+ lineStart = lineEnd.lineEnd;
51
+ continue;
52
+ }
53
+ const bodyStart = getNextBodyStart(markdown, lineEnd.lineEnd);
54
+ const raw = markdown.slice(0, bodyStart);
55
+ const yaml = markdown.slice(openingLine.lineEnd, lineStart);
56
+ const trailingWhitespace = markdown.slice(lineEnd.lineEnd, bodyStart);
57
+ try {
58
+ const data = parseYamlFrontMatter(yaml);
59
+ return {
60
+ body: markdown.slice(bodyStart),
61
+ errors: [],
62
+ frontMatter: {
63
+ data,
64
+ raw,
65
+ trailingWhitespace,
66
+ yaml,
67
+ },
68
+ };
69
+ }
70
+ catch (error) {
71
+ return {
72
+ body: markdown,
73
+ errors: [error instanceof Error ? error.message : String(error)],
74
+ frontMatter: null,
75
+ };
76
+ }
77
+ }
78
+ return { body: markdown, errors: [], frontMatter: null };
79
+ };
80
+ export const hasYamlFrontMatter = (markdown) => extractYamlFrontMatter(markdown).frontMatter !== null;
81
+ export const removeYamlFrontMatter = (markdown) => extractYamlFrontMatter(markdown).body;
82
+ export const prependYamlFrontMatter = (frontMatter, body) => {
83
+ if (!frontMatter) {
84
+ return body;
85
+ }
86
+ const raw = typeof frontMatter === "string" ? frontMatter : frontMatter.raw;
87
+ return `${raw}${body}`;
88
+ };
@@ -1,13 +1,13 @@
1
1
  export const defaultMdKitEditorTheme = {
2
2
  background: "#ffffff",
3
- blockGap: "0.75rem",
3
+ blockGap: "0.72em",
4
4
  border: "#d8dee8",
5
5
  codeBackground: "#eef1f4",
6
6
  codeRadius: "0.35rem",
7
7
  fontFamily: "inherit",
8
8
  fontSize: "16px",
9
9
  foreground: "#18212f",
10
- lineHeight: "1.7",
10
+ lineHeight: "1.55",
11
11
  link: "#4f46e5",
12
12
  muted: "#eef1f4",
13
13
  mutedForeground: "#5b6472",
@@ -15,14 +15,14 @@ export const defaultMdKitEditorTheme = {
15
15
  };
16
16
  export const darkMdKitEditorTheme = {
17
17
  background: "#0b1220",
18
- blockGap: "0.75rem",
18
+ blockGap: "0.72em",
19
19
  border: "#314158",
20
20
  codeBackground: "#111827",
21
21
  codeRadius: "0.35rem",
22
22
  fontFamily: "inherit",
23
23
  fontSize: "16px",
24
24
  foreground: "#e5edf7",
25
- lineHeight: "1.7",
25
+ lineHeight: "1.55",
26
26
  link: "#38bdf8",
27
27
  muted: "#172033",
28
28
  mutedForeground: "#94a3b8",
@@ -1,6 +1,7 @@
1
1
  import * as Y from "yjs";
2
2
  export type MdKitMarkdownYjsOptions = {
3
3
  fragmentName?: string;
4
+ ignoreYamlFrontMatter?: boolean;
4
5
  };
5
6
  export declare const replaceMdKitYjsMarkdown: (ydoc: Y.Doc, markdown: string, options?: MdKitMarkdownYjsOptions) => Uint8Array;
6
7
  export declare const markdownToMdKitYjs: (markdown: string, options?: MdKitMarkdownYjsOptions) => Uint8Array;
@@ -3,10 +3,14 @@ import { MarkdownManager } from "@tiptap/markdown";
3
3
  import { prosemirrorJSONToYXmlFragment, yXmlFragmentToProsemirrorJSON, } from "@tiptap/y-tiptap";
4
4
  import * as Y from "yjs";
5
5
  import { createMdKitTiptapExtensions } from "../markdown/createMdKitTiptapExtensions.js";
6
+ import { extractYamlFrontMatter, prependYamlFrontMatter, } from "../markdown/yamlFrontMatter.js";
6
7
  import { normalizeMarkdownSerialization } from "../markdown/normalizeMarkdownSerialization.js";
7
8
  import { prepareMarkdownForEditorHydration } from "../markdown/prepareMarkdownForEditorHydration.js";
8
9
  const defaultMdKitYjsFragmentName = "default";
10
+ const mdKitYjsMetadataMapName = "__mdkit";
11
+ const frontMatterPrefixMetadataKey = "frontMatterPrefix";
9
12
  const getMdKitYjsFragmentName = (options) => options?.fragmentName ?? defaultMdKitYjsFragmentName;
13
+ const getFrontMatterPrefixMetadataKey = (fragmentName) => `${fragmentName}:${frontMatterPrefixMetadataKey}`;
10
14
  const createMdKitMarkdownManager = () => new MarkdownManager({
11
15
  extensions: createMdKitTiptapExtensions(),
12
16
  markedOptions: {
@@ -17,10 +21,22 @@ const createMdKitProseMirrorSchema = () => getSchema(createMdKitTiptapExtensions
17
21
  const markdownToProseMirrorJson = (markdown) => createMdKitMarkdownManager().parse(prepareMarkdownForEditorHydration(markdown));
18
22
  const proseMirrorJsonToMarkdown = (json) => normalizeMarkdownSerialization(createMdKitMarkdownManager().serialize(json));
19
23
  export const replaceMdKitYjsMarkdown = (ydoc, markdown, options) => {
20
- const fragment = ydoc.getXmlFragment(getMdKitYjsFragmentName(options));
24
+ const fragmentName = getMdKitYjsFragmentName(options);
25
+ const fragment = ydoc.getXmlFragment(fragmentName);
26
+ const metadata = ydoc.getMap(mdKitYjsMetadataMapName);
21
27
  const schema = createMdKitProseMirrorSchema();
22
- const json = markdownToProseMirrorJson(markdown);
28
+ const frontMatter = options?.ignoreYamlFrontMatter
29
+ ? extractYamlFrontMatter(markdown)
30
+ : null;
31
+ const json = markdownToProseMirrorJson(frontMatter?.body ?? markdown);
32
+ const metadataKey = getFrontMatterPrefixMetadataKey(fragmentName);
23
33
  prosemirrorJSONToYXmlFragment(schema, json, fragment);
34
+ if (frontMatter?.frontMatter) {
35
+ metadata.set(metadataKey, frontMatter.frontMatter.raw);
36
+ }
37
+ else {
38
+ metadata.delete(metadataKey);
39
+ }
24
40
  return Y.encodeStateAsUpdate(ydoc);
25
41
  };
26
42
  export const markdownToMdKitYjs = (markdown, options) => {
@@ -30,8 +46,11 @@ export const markdownToMdKitYjs = (markdown, options) => {
30
46
  export const mdKitYjsToMarkdown = (yjsState, options) => {
31
47
  const ydoc = new Y.Doc();
32
48
  Y.applyUpdate(ydoc, yjsState);
33
- const json = yXmlFragmentToProsemirrorJSON(ydoc.getXmlFragment(getMdKitYjsFragmentName(options)));
34
- return proseMirrorJsonToMarkdown(json);
49
+ const fragmentName = getMdKitYjsFragmentName(options);
50
+ const json = yXmlFragmentToProsemirrorJSON(ydoc.getXmlFragment(fragmentName));
51
+ const metadata = ydoc.getMap(mdKitYjsMetadataMapName);
52
+ const frontMatterRaw = metadata.get(getFrontMatterPrefixMetadataKey(fragmentName)) ?? "";
53
+ return prependYamlFrontMatter(frontMatterRaw, proseMirrorJsonToMarkdown(json));
35
54
  };
36
55
  export const yjs = {
37
56
  markdownToMdKitYjs,
package/docs/api.md CHANGED
@@ -39,6 +39,7 @@ Local editing props:
39
39
  - `onChange?: (markdown: string) => void`
40
40
  - `onFocusChange?: (focused: boolean) => void`
41
41
  - `fillHeight?: boolean`
42
+ - `search?: boolean`
42
43
  - `instanceKey?: string | number`
43
44
  - `className?: string`
44
45
  - `style?: CSSProperties`
@@ -50,6 +51,7 @@ Collaborative editing props:
50
51
  - `onChange?: (markdown: string) => void`
51
52
  - `onFocusChange?: (focused: boolean) => void`
52
53
  - `fillHeight?: boolean`
54
+ - `search?: boolean`
53
55
  - `className?: string`
54
56
  - `style?: CSSProperties`
55
57
 
@@ -57,6 +59,10 @@ Collaborative editing props:
57
59
  keep blank space below the last line clickable so it focuses the cursor at the
58
60
  end. Leave it off when the host application owns sizing and scrolling.
59
61
 
62
+ `search` opts the editor into the built-in document search panel. The panel is
63
+ not rendered by default; when enabled, users open it with `Cmd+F` on macOS or
64
+ `Ctrl+F` on Windows/Linux.
65
+
60
66
  The package stylesheet includes reset-resistant markdown rules for headings,
61
67
  lists, code blocks, blockquotes, and links. Styling is controlled with CSS
62
68
  variables on `.mp-lb-mdkit-markdown-editor`. See [Styling](./styling.md) for setup,
package/docs/styling.md CHANGED
@@ -120,7 +120,8 @@ you need structural changes, component-specific spacing, or state styling.
120
120
 
121
121
  `MdKitEditor` renders the markdown editing surface and the selection bubble
122
122
  toolbar. The toolbar appears for non-empty text selections while the editor or
123
- toolbar has focus.
123
+ toolbar has focus. The search panel appears only when `search` is enabled and
124
+ the user opens it with the find keyboard shortcut.
124
125
 
125
126
  - `.mp-lb-mdkit-markdown-editor`: root element rendered by `MdKitEditor`
126
127
  - `.mp-lb-mdkit-markdown-editor-fill-height`: added to the root when
@@ -129,6 +130,10 @@ toolbar has focus.
129
130
  - `.mp-lb-mdkit-editor-surface`: scroll and background surface around the
130
131
  ProseMirror editor
131
132
  - `.mp-lb-mdkit-editor-empty`: loading or connecting placeholder
133
+ - `.mp-lb-mdkit-search-panel`: optional document search panel
134
+ - `.mp-lb-mdkit-search-input`: search text input
135
+ - `.mp-lb-mdkit-search-status`: search result count
136
+ - `.mp-lb-mdkit-search-button`: search navigation or close button
132
137
  - `.mp-lb-mdkit-tiptap`: ProseMirror editable element
133
138
  - `.mp-lb-mdkit-toolbar`: selection bubble toolbar
134
139
  - `.mp-lb-mdkit-toolbar-button`: toolbar button
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mp-lb/mdkit",
3
- "version": "0.2.5-main.27.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -94,6 +94,7 @@
94
94
  "lucide-react": "^0.554.0",
95
95
  "react-markdown": "10.1.0",
96
96
  "remark-gfm": "4.0.1",
97
+ "yaml": "2.9.0",
97
98
  "yjs": "^13.6.24",
98
99
  "zod": "^4.1.12"
99
100
  },
package/src/styles.css CHANGED
@@ -19,18 +19,38 @@
19
19
  --mp-lb-mdkit-muted: var(--muted, #f3f4f6);
20
20
  --mp-lb-mdkit-muted-foreground: var(--muted-foreground, #6b7280);
21
21
  --mp-lb-mdkit-border: var(--border, #e5e7eb);
22
+ --mp-lb-mdkit-input: var(--input, var(--mp-lb-mdkit-border));
23
+ --mp-lb-mdkit-input-background: color-mix(
24
+ in srgb,
25
+ var(--mp-lb-mdkit-input) 30%,
26
+ transparent
27
+ );
28
+ --mp-lb-mdkit-input-background-hover: color-mix(
29
+ in srgb,
30
+ var(--mp-lb-mdkit-input) 50%,
31
+ transparent
32
+ );
33
+ --mp-lb-mdkit-popover: var(--popover, var(--mp-lb-mdkit-background));
34
+ --mp-lb-mdkit-popover-foreground: var(
35
+ --popover-foreground,
36
+ var(--mp-lb-mdkit-foreground)
37
+ );
38
+ --mp-lb-mdkit-ring: var(--ring, #94a3b8);
22
39
  --mp-lb-mdkit-accent: var(--primary, #111827);
23
40
  --mp-lb-mdkit-accent-foreground: var(--primary-foreground, #ffffff);
24
41
  --mp-lb-mdkit-link: #4f46e5;
25
42
  --mp-lb-mdkit-font-family: inherit;
26
43
  --mp-lb-mdkit-font-size: 1rem;
27
- --mp-lb-mdkit-line-height: 1.7;
44
+ --mp-lb-mdkit-line-height: 1.55;
28
45
  --mp-lb-mdkit-surface-padding: 1rem;
29
- --mp-lb-mdkit-block-gap: 0.75rem;
46
+ --mp-lb-mdkit-block-gap: 0.72em;
47
+ --mp-lb-mdkit-tight-gap: 0.35em;
48
+ --mp-lb-mdkit-section-gap: 1.25em;
30
49
  --mp-lb-mdkit-list-item-gap: 0.125rem;
31
50
  --mp-lb-mdkit-heading-font-weight: 650;
32
51
  --mp-lb-mdkit-heading-1-size: 1.5rem;
33
52
  --mp-lb-mdkit-heading-2-size: 1.25rem;
53
+ --mp-lb-mdkit-heading-3-size: 1.125rem;
34
54
  --mp-lb-mdkit-code-background: var(--mp-lb-mdkit-muted);
35
55
  --mp-lb-mdkit-code-radius: 0.35rem;
36
56
  --mp-lb-mdkit-code-block-radius: 0.75rem;
@@ -108,6 +128,7 @@
108
128
  }
109
129
 
110
130
  .mp-lb-mdkit-editor-surface {
131
+ position: relative;
111
132
  width: 100%;
112
133
  min-height: 0;
113
134
  overflow: visible;
@@ -115,6 +136,168 @@
115
136
  padding: var(--mp-lb-mdkit-surface-padding);
116
137
  }
117
138
 
139
+ .mp-lb-mdkit-search-panel {
140
+ position: sticky;
141
+ z-index: 5;
142
+ top: 0.5rem;
143
+ margin: calc(var(--mp-lb-mdkit-surface-padding) * -0.5) 0
144
+ var(--mp-lb-mdkit-block-gap) auto;
145
+ width: min(100%, 27rem);
146
+ min-height: 2.75rem;
147
+ display: flex;
148
+ align-items: center;
149
+ gap: 0.375rem;
150
+ border: 0;
151
+ border-radius: var(--radius-xl, 0.875rem);
152
+ background: var(--mp-lb-mdkit-popover);
153
+ box-shadow:
154
+ 0 1px 2px rgb(15 23 42 / 6%),
155
+ 0 0.5rem 1.25rem rgb(15 23 42 / 10%);
156
+ color: var(--mp-lb-mdkit-popover-foreground);
157
+ padding: 0.375rem;
158
+ }
159
+
160
+ .mp-lb-mdkit-search-icon {
161
+ width: 1rem;
162
+ height: 1rem;
163
+ flex: 0 0 auto;
164
+ color: var(--mp-lb-mdkit-muted-foreground);
165
+ margin-left: 0.5rem;
166
+ }
167
+
168
+ .mp-lb-mdkit-search-input {
169
+ height: 2rem;
170
+ min-width: 0;
171
+ flex: 1 1 auto;
172
+ border: 0;
173
+ border-radius: var(--radius-md, 0.5rem);
174
+ background: var(--mp-lb-mdkit-input-background);
175
+ color: inherit;
176
+ font: inherit;
177
+ font-size: 0.875rem;
178
+ line-height: 1.25rem;
179
+ outline: none;
180
+ padding: 0 0.5rem;
181
+ transition:
182
+ background-color 120ms ease,
183
+ box-shadow 120ms ease;
184
+ }
185
+
186
+ .mp-lb-mdkit-markdown-editor .mp-lb-mdkit-search-input {
187
+ border-color: transparent;
188
+ border-style: solid;
189
+ border-width: 0;
190
+ box-shadow: none;
191
+ outline: none;
192
+ }
193
+
194
+ .mp-lb-mdkit-search-input::placeholder {
195
+ color: var(--mp-lb-mdkit-muted-foreground);
196
+ }
197
+
198
+ .mp-lb-mdkit-search-input:focus {
199
+ outline: none;
200
+ background: var(--mp-lb-mdkit-input-background-hover);
201
+ }
202
+
203
+ .mp-lb-mdkit-markdown-editor .mp-lb-mdkit-search-input:focus,
204
+ .mp-lb-mdkit-markdown-editor .mp-lb-mdkit-search-input:focus-visible {
205
+ border-color: transparent;
206
+ border-width: 0;
207
+ box-shadow: none;
208
+ outline: none;
209
+ }
210
+
211
+ .mp-lb-mdkit-search-input::-webkit-search-cancel-button {
212
+ appearance: none;
213
+ width: 1rem;
214
+ height: 1rem;
215
+ background-color: var(--mp-lb-mdkit-muted-foreground);
216
+ cursor: pointer;
217
+ opacity: 0.75;
218
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='black' d='M4.22 3.28a.75.75 0 0 0-.94.94L7.06 8l-3.78 3.78a.75.75 0 1 0 .94.94L8 8.94l3.78 3.78a.75.75 0 1 0 .94-.94L8.94 8l3.78-3.78a.75.75 0 1 0-.94-.94L8 7.06 4.22 3.28Z'/%3E%3C/svg%3E")
219
+ center / 1rem 1rem no-repeat;
220
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath fill='black' d='M4.22 3.28a.75.75 0 0 0-.94.94L7.06 8l-3.78 3.78a.75.75 0 1 0 .94.94L8 8.94l3.78 3.78a.75.75 0 1 0 .94-.94L8.94 8l3.78-3.78a.75.75 0 1 0-.94-.94L8 7.06 4.22 3.28Z'/%3E%3C/svg%3E")
221
+ center / 1rem 1rem no-repeat;
222
+ }
223
+
224
+ .mp-lb-mdkit-search-input::-webkit-search-cancel-button:hover {
225
+ opacity: 1;
226
+ }
227
+
228
+ .mp-lb-mdkit-search-status {
229
+ flex: 0 0 auto;
230
+ color: var(--mp-lb-mdkit-muted-foreground);
231
+ font-size: 0.75rem;
232
+ line-height: 1rem;
233
+ padding: 0 0.375rem;
234
+ white-space: nowrap;
235
+ }
236
+
237
+ .mp-lb-mdkit-search-button {
238
+ width: 2rem;
239
+ height: 2rem;
240
+ display: inline-flex;
241
+ align-items: center;
242
+ justify-content: center;
243
+ flex: 0 0 auto;
244
+ border: 0;
245
+ border-radius: var(--radius-4xl, 999px);
246
+ background: transparent;
247
+ color: var(--mp-lb-mdkit-muted-foreground);
248
+ cursor: pointer;
249
+ padding: 0;
250
+ transition:
251
+ background-color 120ms ease,
252
+ color 120ms ease,
253
+ box-shadow 120ms ease;
254
+ }
255
+
256
+ .mp-lb-mdkit-search-button:hover:not(:disabled),
257
+ .mp-lb-mdkit-search-button:focus-visible {
258
+ background: var(--mp-lb-mdkit-input-background-hover);
259
+ color: var(--mp-lb-mdkit-foreground);
260
+ }
261
+
262
+ .mp-lb-mdkit-search-button:focus-visible {
263
+ outline: none;
264
+ box-shadow: 0 0 0 3px
265
+ color-mix(in srgb, var(--mp-lb-mdkit-ring) 50%, transparent);
266
+ }
267
+
268
+ .mp-lb-mdkit-search-button:disabled {
269
+ cursor: default;
270
+ opacity: 0.45;
271
+ }
272
+
273
+ .mp-lb-mdkit-search-button svg {
274
+ width: 1rem;
275
+ height: 1rem;
276
+ }
277
+
278
+ .mp-lb-mdkit-search-match {
279
+ border-radius: 0;
280
+ background: transparent;
281
+ box-shadow: inset 0 -3px 0 #3b82f6;
282
+ }
283
+
284
+ .mp-lb-mdkit-search-match-active {
285
+ background: transparent;
286
+ box-shadow: inset 0 -3px 0 #2563eb;
287
+ }
288
+
289
+ @media (max-width: 42rem) {
290
+ .mp-lb-mdkit-search-panel {
291
+ width: 100%;
292
+ }
293
+
294
+ .mp-lb-mdkit-search-status {
295
+ max-width: 5.75rem;
296
+ overflow: hidden;
297
+ text-overflow: ellipsis;
298
+ }
299
+ }
300
+
118
301
  .mp-lb-mdkit-markdown-editor-fill-height .mp-lb-mdkit-editor-surface {
119
302
  display: flex;
120
303
  overflow: auto;
@@ -162,34 +345,68 @@
162
345
  flex: 0 0 auto;
163
346
  }
164
347
 
348
+ .mp-lb-mdkit-tiptap
349
+ :is(p, h1, h2, h3, h4, h5, h6, ul, ol, blockquote, pre, table) {
350
+ margin: 0;
351
+ }
352
+
165
353
  .mp-lb-mdkit-tiptap > * + * {
166
354
  margin-top: var(--mp-lb-mdkit-block-gap);
167
355
  }
168
356
 
357
+ .mp-lb-mdkit-tiptap h1,
358
+ .mp-lb-mdkit-tiptap h2,
359
+ .mp-lb-mdkit-tiptap h3,
360
+ .mp-lb-mdkit-tiptap h4,
361
+ .mp-lb-mdkit-tiptap h5,
362
+ .mp-lb-mdkit-tiptap h6 {
363
+ font-weight: var(--mp-lb-mdkit-heading-font-weight);
364
+ }
365
+
169
366
  .mp-lb-mdkit-tiptap h1 {
170
- margin: 0 0 var(--mp-lb-mdkit-block-gap);
171
367
  font-size: var(--mp-lb-mdkit-heading-1-size);
172
- font-weight: var(--mp-lb-mdkit-heading-font-weight);
173
368
  line-height: 1.25;
174
369
  }
175
370
 
176
371
  .mp-lb-mdkit-tiptap h2 {
177
- margin: 0 0 var(--mp-lb-mdkit-block-gap);
178
372
  font-size: var(--mp-lb-mdkit-heading-2-size);
179
- font-weight: var(--mp-lb-mdkit-heading-font-weight);
180
373
  line-height: 1.3;
181
374
  }
182
375
 
183
- .mp-lb-mdkit-tiptap p {
184
- margin: 0 0 var(--mp-lb-mdkit-block-gap);
376
+ .mp-lb-mdkit-tiptap h3,
377
+ .mp-lb-mdkit-tiptap h4,
378
+ .mp-lb-mdkit-tiptap h5,
379
+ .mp-lb-mdkit-tiptap h6 {
380
+ font-size: var(--mp-lb-mdkit-heading-3-size);
381
+ line-height: 1.35;
185
382
  }
186
383
 
187
384
  .mp-lb-mdkit-tiptap ul,
188
385
  .mp-lb-mdkit-tiptap ol {
189
- margin: 0 0 var(--mp-lb-mdkit-block-gap);
386
+ margin: 0;
190
387
  padding-left: 1.5rem;
191
388
  }
192
389
 
390
+ .mp-lb-mdkit-tiptap
391
+ > :is(h1, h2, h3)
392
+ + :is(p, ul, ol, blockquote, pre, table) {
393
+ margin-top: var(--mp-lb-mdkit-tight-gap);
394
+ }
395
+
396
+ .mp-lb-mdkit-tiptap > p + p {
397
+ margin-top: var(--mp-lb-mdkit-block-gap);
398
+ }
399
+
400
+ .mp-lb-mdkit-tiptap > :is(p, ul, ol) + :is(ul, ol, p) {
401
+ margin-top: var(--mp-lb-mdkit-tight-gap);
402
+ }
403
+
404
+ .mp-lb-mdkit-tiptap
405
+ > :is(p, ul, ol, blockquote, pre, table, hr)
406
+ + :is(h1, h2, h3) {
407
+ margin-top: var(--mp-lb-mdkit-section-gap);
408
+ }
409
+
193
410
  .mp-lb-mdkit-tiptap ul {
194
411
  list-style: disc;
195
412
  list-style-position: outside;
@@ -220,7 +437,7 @@
220
437
  }
221
438
 
222
439
  .mp-lb-mdkit-tiptap pre {
223
- margin: 0 0 var(--mp-lb-mdkit-block-gap);
440
+ margin: 0;
224
441
  padding: 0.75rem 0.875rem;
225
442
  overflow-x: auto;
226
443
  border: 1px solid var(--mp-lb-mdkit-border);
@@ -242,7 +459,7 @@
242
459
  }
243
460
 
244
461
  .mp-lb-mdkit-tiptap blockquote {
245
- margin: 0 0 var(--mp-lb-mdkit-block-gap);
462
+ margin: 0;
246
463
  padding-left: 0.875rem;
247
464
  border-left: 3px solid var(--mp-lb-mdkit-quote-border-color);
248
465
  color: var(--mp-lb-mdkit-muted-foreground);
@@ -252,7 +469,7 @@
252
469
  height: 1px;
253
470
  border: 0;
254
471
  background: var(--mp-lb-mdkit-border);
255
- margin: var(--mp-lb-mdkit-block-gap) 0;
472
+ margin: var(--mp-lb-mdkit-section-gap) 0;
256
473
  }
257
474
 
258
475
  .mp-lb-mdkit-tiptap img {
@@ -263,7 +480,7 @@
263
480
  .mp-lb-mdkit-tiptap table {
264
481
  width: 100%;
265
482
  border-collapse: collapse;
266
- margin: 0 0 var(--mp-lb-mdkit-block-gap);
483
+ margin: 0;
267
484
  }
268
485
 
269
486
  .mp-lb-mdkit-tiptap th,