@mp-lb/mdkit 0.2.5 → 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/README.md +1 -0
- package/dist/document/useMdKitDocument.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/markdown/MarkdownPasteExtension.d.ts +3 -0
- package/dist/markdown/MarkdownPasteExtension.js +62 -0
- package/dist/markdown/MarkdownSearchExtension.d.ts +9 -0
- package/dist/markdown/MarkdownSearchExtension.js +42 -0
- package/dist/markdown/MarkdownSearchPanel.d.ts +13 -0
- package/dist/markdown/MarkdownSearchPanel.js +25 -0
- package/dist/markdown/MdKitEditor.d.ts +2 -0
- package/dist/markdown/MdKitView.d.ts +2 -1
- package/dist/markdown/MdKitView.js +6 -2
- package/dist/markdown/TiptapMarkdownSurface.d.ts +4 -0
- package/dist/markdown/TiptapMarkdownSurface.js +168 -7
- package/dist/markdown/createMdKitTiptapExtensions.js +4 -0
- package/dist/markdown/yamlFrontMatter.d.ts +16 -0
- package/dist/markdown/yamlFrontMatter.js +88 -0
- package/dist/theme/editorTheme.js +4 -4
- package/dist/yjs/MdKitMarkdownYjs.d.ts +1 -0
- package/dist/yjs/MdKitMarkdownYjs.js +23 -4
- package/docs/.vitepress/config.ts +2 -0
- package/docs/api.md +6 -0
- package/docs/index.md +5 -1
- package/docs/plain-text.md +131 -0
- package/docs/shadcn.md +5 -1
- package/docs/styling.md +6 -1
- package/package.json +2 -1
- package/src/styles.css +230 -13
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ const emptyDocumentState = {
|
|
|
5
5
|
version: null,
|
|
6
6
|
};
|
|
7
7
|
export const useMdKitDocument = (options) => {
|
|
8
|
-
const { adapter, debounceMs =
|
|
8
|
+
const { adapter, debounceMs = 1000, documentId, pollMs = 2000 } = options;
|
|
9
9
|
const [local, setLocal] = useState("");
|
|
10
10
|
const [base, setBase] = useState("");
|
|
11
11
|
const [version, setVersion] = useState(null);
|
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,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
|
-
|
|
6
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
24
|
+
const fragmentName = getMdKitYjsFragmentName(options);
|
|
25
|
+
const fragment = ydoc.getXmlFragment(fragmentName);
|
|
26
|
+
const metadata = ydoc.getMap(mdKitYjsMetadataMapName);
|
|
21
27
|
const schema = createMdKitProseMirrorSchema();
|
|
22
|
-
const
|
|
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
|
|
34
|
-
|
|
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,
|
|
@@ -7,6 +7,7 @@ export default defineConfig({
|
|
|
7
7
|
themeConfig: {
|
|
8
8
|
nav: [
|
|
9
9
|
{ text: "Quick Start", link: "/" },
|
|
10
|
+
{ text: "Plain Text", link: "/plain-text" },
|
|
10
11
|
{ text: "Styling", link: "/styling" },
|
|
11
12
|
{ text: "Shadcn", link: "/shadcn" },
|
|
12
13
|
{ text: "REST", link: "/rest" },
|
|
@@ -21,6 +22,7 @@ export default defineConfig({
|
|
|
21
22
|
text: "Guide",
|
|
22
23
|
items: [
|
|
23
24
|
{ text: "Quick Start", link: "/" },
|
|
25
|
+
{ text: "Plain Text Editors", link: "/plain-text" },
|
|
24
26
|
{ text: "Styling", link: "/styling" },
|
|
25
27
|
{ text: "Shadcn Plugin", link: "/shadcn" },
|
|
26
28
|
{ text: "REST Backend", link: "/rest" },
|
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/index.md
CHANGED
|
@@ -113,7 +113,11 @@ export function ConnectedMarkdownEditor({
|
|
|
113
113
|
() => createMdKitTrpcAdapter({ client: trpc.mdkit }),
|
|
114
114
|
[trpc],
|
|
115
115
|
);
|
|
116
|
-
const document = useMdKitDocument({
|
|
116
|
+
const document = useMdKitDocument({
|
|
117
|
+
adapter,
|
|
118
|
+
debounceMs: 1000,
|
|
119
|
+
documentId,
|
|
120
|
+
});
|
|
117
121
|
const versions = useMdKitDocumentVersions({ adapter, documentId });
|
|
118
122
|
|
|
119
123
|
const collaboration = useMdKitCollaboration({
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Plain Text Editors
|
|
2
|
+
|
|
3
|
+
MDKit's connected workflow is not limited to `MdKitEditor`. The document hooks
|
|
4
|
+
and backend adapters work with serialized text, so you can bring a plain text,
|
|
5
|
+
code, JSON, or custom text editor and still use the same storage, autosave,
|
|
6
|
+
checkpoint history, restore, and conflict handling.
|
|
7
|
+
|
|
8
|
+
The one major exception is collaboration. Collaboration is currently a
|
|
9
|
+
markdown/Tiptap capability because it depends on Yjs, ProseMirror, and the
|
|
10
|
+
Tiptap collaboration extensions.
|
|
11
|
+
|
|
12
|
+
## What Works
|
|
13
|
+
|
|
14
|
+
Any editor can plug into the connected workflow if it behaves like a controlled
|
|
15
|
+
text input:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
type TextEditorProps = {
|
|
19
|
+
value: string;
|
|
20
|
+
onChange(value: string): void;
|
|
21
|
+
onFocusChange?(focused: boolean): void;
|
|
22
|
+
readOnly?: boolean;
|
|
23
|
+
};
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
That is enough for:
|
|
27
|
+
|
|
28
|
+
- loading the current document
|
|
29
|
+
- autosave
|
|
30
|
+
- dirty state
|
|
31
|
+
- conflict detection
|
|
32
|
+
- force save
|
|
33
|
+
- remote resync
|
|
34
|
+
- checkpoint history
|
|
35
|
+
- checkpoint restore
|
|
36
|
+
|
|
37
|
+
The editor does not need to know about MDKit internals. It only needs to receive
|
|
38
|
+
`document.value` and call `document.setContent`.
|
|
39
|
+
|
|
40
|
+
## Example
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import {
|
|
44
|
+
MdKitConflictPanel,
|
|
45
|
+
MdKitDocumentToolbar,
|
|
46
|
+
VersionHistoryPanel,
|
|
47
|
+
useMdKitDocument,
|
|
48
|
+
useMdKitDocumentVersions,
|
|
49
|
+
type MdKitDocumentAdapter,
|
|
50
|
+
} from "@mp-lb/mdkit";
|
|
51
|
+
|
|
52
|
+
function PlainTextDocument({
|
|
53
|
+
adapter,
|
|
54
|
+
documentId,
|
|
55
|
+
}: {
|
|
56
|
+
adapter: MdKitDocumentAdapter;
|
|
57
|
+
documentId: string;
|
|
58
|
+
}) {
|
|
59
|
+
const document = useMdKitDocument({
|
|
60
|
+
adapter,
|
|
61
|
+
debounceMs: 1000,
|
|
62
|
+
documentId,
|
|
63
|
+
});
|
|
64
|
+
const versions = useMdKitDocumentVersions({ adapter, documentId });
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<MdKitDocumentToolbar document={document} versions={versions} />
|
|
69
|
+
|
|
70
|
+
<textarea
|
|
71
|
+
readOnly={document.conflict}
|
|
72
|
+
value={document.value}
|
|
73
|
+
onBlur={() => document.setFocused(false)}
|
|
74
|
+
onChange={(event) => document.setContent(event.currentTarget.value)}
|
|
75
|
+
onFocus={() => document.setFocused(true)}
|
|
76
|
+
/>
|
|
77
|
+
|
|
78
|
+
<MdKitConflictPanel document={document} />
|
|
79
|
+
<VersionHistoryPanel controller={versions} />
|
|
80
|
+
</>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Use the same backend adapter you would use for markdown. The document content is
|
|
86
|
+
still just `content: string`.
|
|
87
|
+
|
|
88
|
+
## Backend Shape
|
|
89
|
+
|
|
90
|
+
You do not need a separate backend for plain text documents. A single MDKit
|
|
91
|
+
backend can expose:
|
|
92
|
+
|
|
93
|
+
- document read/write
|
|
94
|
+
- checkpoint list/read/restore
|
|
95
|
+
- optional collaboration websocket routes
|
|
96
|
+
- optional collaboration state persistence
|
|
97
|
+
|
|
98
|
+
Plain text editors use the document and checkpoint APIs. Markdown collaborative
|
|
99
|
+
editors additionally use the collaboration websocket and Yjs persistence.
|
|
100
|
+
|
|
101
|
+
The underlying database layout is application-owned. It is reasonable to store
|
|
102
|
+
markdown and plain text documents in the same documents table, or in separate
|
|
103
|
+
tables if your product needs that. MDKit only requires a stable `documentId`,
|
|
104
|
+
`content`, and an opaque revision token.
|
|
105
|
+
|
|
106
|
+
## Collaboration Boundary
|
|
107
|
+
|
|
108
|
+
Do not pass `useMdKitCollaboration` to a plain text editor. The current
|
|
109
|
+
collaboration adapter is for `MdKitEditor` because that editor knows how to bind
|
|
110
|
+
Tiptap to a Yjs document and render remote cursors.
|
|
111
|
+
|
|
112
|
+
For plain text documents:
|
|
113
|
+
|
|
114
|
+
- keep using `useMdKitDocument`
|
|
115
|
+
- omit `useMdKitCollaboration`
|
|
116
|
+
- omit collaboration UI
|
|
117
|
+
- rely on optimistic conflicts and resync for multi-client safety
|
|
118
|
+
|
|
119
|
+
If MDKit later adds a collaboration-capable CodeMirror, Monaco, or textarea
|
|
120
|
+
adapter, that should be a new editor-specific capability. The generic text
|
|
121
|
+
workflow does not need to change.
|
|
122
|
+
|
|
123
|
+
## Testbench
|
|
124
|
+
|
|
125
|
+
The testbench includes a connected stack named
|
|
126
|
+
`Storage + checkpoints (plain text)`. It reuses the same checkpoints backend as
|
|
127
|
+
the markdown stack, stores content under `docs/plain-text.txt`, and renders a
|
|
128
|
+
controlled textarea instead of `MdKitEditor`.
|
|
129
|
+
|
|
130
|
+
Use it to verify that plain text can autosave, create checkpoints, restore
|
|
131
|
+
history, and avoid collaboration UI.
|
package/docs/shadcn.md
CHANGED
|
@@ -46,7 +46,11 @@ import { MdKitConnectedWorkflow } from "@/components/mdkit/mdkit-connected-workf
|
|
|
46
46
|
export function EditorScreen() {
|
|
47
47
|
const client = createMdKitTrpcClient({ url: "/trpc" });
|
|
48
48
|
const adapter = createMdKitTrpcAdapter({ client });
|
|
49
|
-
const document = useMdKitDocument({
|
|
49
|
+
const document = useMdKitDocument({
|
|
50
|
+
adapter,
|
|
51
|
+
debounceMs: 1000,
|
|
52
|
+
documentId,
|
|
53
|
+
});
|
|
50
54
|
const versions = useMdKitDocumentVersions({ adapter, documentId });
|
|
51
55
|
|
|
52
56
|
const collaboration = useMdKitCollaboration({
|
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.
|
|
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.
|
|
44
|
+
--mp-lb-mdkit-line-height: 1.55;
|
|
28
45
|
--mp-lb-mdkit-surface-padding: 1rem;
|
|
29
|
-
--mp-lb-mdkit-block-gap: 0.
|
|
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
|
|
184
|
-
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
483
|
+
margin: 0;
|
|
267
484
|
}
|
|
268
485
|
|
|
269
486
|
.mp-lb-mdkit-tiptap th,
|