@idealyst/markdown 1.2.37 → 1.2.39
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/package.json +74 -5
- package/src/Editor/EditorToolbar.web.tsx +90 -0
- package/src/Editor/MarkdownEditor.native.tsx +262 -0
- package/src/Editor/MarkdownEditor.styles.ts +227 -0
- package/src/Editor/MarkdownEditor.web.tsx +331 -0
- package/src/Editor/examples/MarkdownEditorExamples.tsx +205 -0
- package/src/Editor/examples/index.ts +1 -0
- package/src/Editor/index.native.ts +23 -0
- package/src/Editor/index.ts +43 -0
- package/src/Editor/types.ts +193 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@idealyst/markdown",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"description": "Cross-platform markdown renderer for React and React Native with theme integration",
|
|
3
|
+
"version": "1.2.39",
|
|
4
|
+
"description": "Cross-platform markdown renderer and editor for React and React Native with theme integration",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"module": "src/index.ts",
|
|
7
7
|
"types": "src/index.ts",
|
|
@@ -22,6 +22,17 @@
|
|
|
22
22
|
"import": "./src/index.ts",
|
|
23
23
|
"require": "./src/index.ts",
|
|
24
24
|
"types": "./src/index.ts"
|
|
25
|
+
},
|
|
26
|
+
"./editor": {
|
|
27
|
+
"react-native": "./src/Editor/index.native.ts",
|
|
28
|
+
"import": "./src/Editor/index.ts",
|
|
29
|
+
"require": "./src/Editor/index.ts",
|
|
30
|
+
"types": "./src/Editor/index.ts"
|
|
31
|
+
},
|
|
32
|
+
"./examples": {
|
|
33
|
+
"import": "./src/Editor/examples/index.ts",
|
|
34
|
+
"require": "./src/Editor/examples/index.ts",
|
|
35
|
+
"types": "./src/Editor/examples/index.ts"
|
|
25
36
|
}
|
|
26
37
|
},
|
|
27
38
|
"scripts": {
|
|
@@ -29,18 +40,53 @@
|
|
|
29
40
|
"publish:npm": "npm publish"
|
|
30
41
|
},
|
|
31
42
|
"peerDependencies": {
|
|
32
|
-
"@
|
|
43
|
+
"@10play/tentap-editor": ">=0.5.0",
|
|
44
|
+
"@idealyst/theme": "^1.2.39",
|
|
45
|
+
"@tiptap/extension-link": ">=2.0.0",
|
|
46
|
+
"@tiptap/extension-placeholder": ">=2.0.0",
|
|
47
|
+
"@tiptap/extension-task-item": ">=2.0.0",
|
|
48
|
+
"@tiptap/extension-task-list": ">=2.0.0",
|
|
49
|
+
"@tiptap/extension-underline": ">=2.0.0",
|
|
50
|
+
"@tiptap/react": ">=2.0.0",
|
|
51
|
+
"@tiptap/starter-kit": ">=2.0.0",
|
|
33
52
|
"react": ">=16.8.0",
|
|
34
53
|
"react-markdown": ">=9.0.0",
|
|
35
54
|
"react-native": ">=0.60.0",
|
|
36
55
|
"react-native-markdown-display": ">=7.0.0",
|
|
37
56
|
"react-native-unistyles": ">=3.0.0",
|
|
38
|
-
"
|
|
57
|
+
"react-native-webview": ">=13.0.0",
|
|
58
|
+
"remark-gfm": ">=4.0.0",
|
|
59
|
+
"showdown": ">=2.0.0",
|
|
60
|
+
"tiptap-markdown": ">=0.8.0"
|
|
39
61
|
},
|
|
40
62
|
"peerDependenciesMeta": {
|
|
63
|
+
"@10play/tentap-editor": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
41
66
|
"@idealyst/theme": {
|
|
42
67
|
"optional": true
|
|
43
68
|
},
|
|
69
|
+
"@tiptap/extension-link": {
|
|
70
|
+
"optional": true
|
|
71
|
+
},
|
|
72
|
+
"@tiptap/extension-placeholder": {
|
|
73
|
+
"optional": true
|
|
74
|
+
},
|
|
75
|
+
"@tiptap/extension-task-item": {
|
|
76
|
+
"optional": true
|
|
77
|
+
},
|
|
78
|
+
"@tiptap/extension-task-list": {
|
|
79
|
+
"optional": true
|
|
80
|
+
},
|
|
81
|
+
"@tiptap/extension-underline": {
|
|
82
|
+
"optional": true
|
|
83
|
+
},
|
|
84
|
+
"@tiptap/react": {
|
|
85
|
+
"optional": true
|
|
86
|
+
},
|
|
87
|
+
"@tiptap/starter-kit": {
|
|
88
|
+
"optional": true
|
|
89
|
+
},
|
|
44
90
|
"react-markdown": {
|
|
45
91
|
"optional": true
|
|
46
92
|
},
|
|
@@ -53,18 +99,39 @@
|
|
|
53
99
|
"react-native-unistyles": {
|
|
54
100
|
"optional": true
|
|
55
101
|
},
|
|
102
|
+
"react-native-webview": {
|
|
103
|
+
"optional": true
|
|
104
|
+
},
|
|
56
105
|
"remark-gfm": {
|
|
57
106
|
"optional": true
|
|
107
|
+
},
|
|
108
|
+
"showdown": {
|
|
109
|
+
"optional": true
|
|
110
|
+
},
|
|
111
|
+
"tiptap-markdown": {
|
|
112
|
+
"optional": true
|
|
58
113
|
}
|
|
59
114
|
},
|
|
60
115
|
"devDependencies": {
|
|
61
|
-
"@
|
|
116
|
+
"@10play/tentap-editor": "^0.5.0",
|
|
117
|
+
"@idealyst/theme": "^1.2.39",
|
|
118
|
+
"@tiptap/extension-link": "^2.11.0",
|
|
119
|
+
"@tiptap/extension-placeholder": "^2.11.0",
|
|
120
|
+
"@tiptap/extension-task-item": "^2.11.0",
|
|
121
|
+
"@tiptap/extension-task-list": "^2.11.0",
|
|
122
|
+
"@tiptap/extension-underline": "^2.11.0",
|
|
123
|
+
"@tiptap/react": "^2.11.0",
|
|
124
|
+
"@tiptap/starter-kit": "^2.11.0",
|
|
62
125
|
"@types/react": "^19.1.0",
|
|
63
126
|
"react": "^19.1.0",
|
|
64
127
|
"react-markdown": "^9.0.0",
|
|
65
128
|
"react-native": "^0.80.1",
|
|
66
129
|
"react-native-unistyles": "^3.0.10",
|
|
130
|
+
"react-native-webview": "^13.0.0",
|
|
67
131
|
"remark-gfm": "^4.0.0",
|
|
132
|
+
"showdown": "^2.1.0",
|
|
133
|
+
"@types/showdown": "^2.0.0",
|
|
134
|
+
"tiptap-markdown": "^0.8.0",
|
|
68
135
|
"typescript": "^5.0.0"
|
|
69
136
|
},
|
|
70
137
|
"files": [
|
|
@@ -75,6 +142,8 @@
|
|
|
75
142
|
"react",
|
|
76
143
|
"react-native",
|
|
77
144
|
"markdown",
|
|
145
|
+
"editor",
|
|
146
|
+
"tiptap",
|
|
78
147
|
"cross-platform",
|
|
79
148
|
"idealyst"
|
|
80
149
|
]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { memo } from 'react';
|
|
2
|
+
import { getWebProps } from 'react-native-unistyles/web';
|
|
3
|
+
import { editorStyles } from './MarkdownEditor.styles';
|
|
4
|
+
import type { ToolbarItem } from './types';
|
|
5
|
+
import type { Size, Intent } from '@idealyst/theme';
|
|
6
|
+
|
|
7
|
+
interface EditorToolbarProps {
|
|
8
|
+
items: readonly ToolbarItem[];
|
|
9
|
+
onAction: (action: string) => void;
|
|
10
|
+
isActive: (action: string) => boolean;
|
|
11
|
+
size: Size;
|
|
12
|
+
linkIntent: Intent;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const TOOLBAR_ICONS: Record<ToolbarItem, string> = {
|
|
16
|
+
bold: 'B',
|
|
17
|
+
italic: 'I',
|
|
18
|
+
underline: 'U',
|
|
19
|
+
strikethrough: 'S',
|
|
20
|
+
code: '</>',
|
|
21
|
+
heading1: 'H1',
|
|
22
|
+
heading2: 'H2',
|
|
23
|
+
heading3: 'H3',
|
|
24
|
+
bulletList: '•',
|
|
25
|
+
orderedList: '1.',
|
|
26
|
+
taskList: '☑',
|
|
27
|
+
blockquote: '"',
|
|
28
|
+
codeBlock: '{ }',
|
|
29
|
+
horizontalRule: '—',
|
|
30
|
+
link: '🔗',
|
|
31
|
+
image: '🖼',
|
|
32
|
+
undo: '↶',
|
|
33
|
+
redo: '↷',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const TOOLBAR_TITLES: Record<ToolbarItem, string> = {
|
|
37
|
+
bold: 'Bold (Ctrl+B)',
|
|
38
|
+
italic: 'Italic (Ctrl+I)',
|
|
39
|
+
underline: 'Underline (Ctrl+U)',
|
|
40
|
+
strikethrough: 'Strikethrough',
|
|
41
|
+
code: 'Inline Code',
|
|
42
|
+
heading1: 'Heading 1',
|
|
43
|
+
heading2: 'Heading 2',
|
|
44
|
+
heading3: 'Heading 3',
|
|
45
|
+
bulletList: 'Bullet List',
|
|
46
|
+
orderedList: 'Numbered List',
|
|
47
|
+
taskList: 'Task List',
|
|
48
|
+
blockquote: 'Blockquote',
|
|
49
|
+
codeBlock: 'Code Block',
|
|
50
|
+
horizontalRule: 'Horizontal Rule',
|
|
51
|
+
link: 'Insert Link',
|
|
52
|
+
image: 'Insert Image',
|
|
53
|
+
undo: 'Undo (Ctrl+Z)',
|
|
54
|
+
redo: 'Redo (Ctrl+Y)',
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const EditorToolbar = memo<EditorToolbarProps>(
|
|
58
|
+
({ items, onAction, isActive, size, linkIntent }) => {
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
{...getWebProps([
|
|
62
|
+
(editorStyles.toolbar as any)({ size, linkIntent }),
|
|
63
|
+
])}
|
|
64
|
+
role="toolbar"
|
|
65
|
+
aria-label="Editor formatting toolbar"
|
|
66
|
+
>
|
|
67
|
+
{items.map((item) => {
|
|
68
|
+
const active = isActive(item);
|
|
69
|
+
return (
|
|
70
|
+
<button
|
|
71
|
+
key={item}
|
|
72
|
+
type="button"
|
|
73
|
+
onClick={() => onAction(item)}
|
|
74
|
+
title={TOOLBAR_TITLES[item]}
|
|
75
|
+
aria-pressed={active}
|
|
76
|
+
{...getWebProps([
|
|
77
|
+
(editorStyles.toolbarButton as any)({ size, linkIntent }),
|
|
78
|
+
active && (editorStyles.toolbarButtonActive as any)({ size, linkIntent }),
|
|
79
|
+
])}
|
|
80
|
+
>
|
|
81
|
+
{TOOLBAR_ICONS[item]}
|
|
82
|
+
</button>
|
|
83
|
+
);
|
|
84
|
+
})}
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
EditorToolbar.displayName = 'EditorToolbar';
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useImperativeHandle,
|
|
4
|
+
useEffect,
|
|
5
|
+
useRef,
|
|
6
|
+
} from 'react';
|
|
7
|
+
import { View, StyleSheet, KeyboardAvoidingView, Platform } from 'react-native';
|
|
8
|
+
import {
|
|
9
|
+
RichText,
|
|
10
|
+
Toolbar,
|
|
11
|
+
useEditorBridge,
|
|
12
|
+
DEFAULT_TOOLBAR_ITEMS,
|
|
13
|
+
type EditorBridge,
|
|
14
|
+
} from '@10play/tentap-editor';
|
|
15
|
+
import Showdown from 'showdown';
|
|
16
|
+
import { editorStyles } from './MarkdownEditor.styles';
|
|
17
|
+
import type { MarkdownEditorProps, MarkdownEditorRef, ToolbarItem } from './types';
|
|
18
|
+
|
|
19
|
+
// Configure Showdown converter with GFM support
|
|
20
|
+
const showdownConverter = new Showdown.Converter({
|
|
21
|
+
tables: true,
|
|
22
|
+
strikethrough: true,
|
|
23
|
+
tasklists: true,
|
|
24
|
+
ghCodeBlocks: true,
|
|
25
|
+
smoothLivePreview: true,
|
|
26
|
+
simpleLineBreaks: false,
|
|
27
|
+
openLinksInNewWindow: false,
|
|
28
|
+
backslashEscapesHTMLTags: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Map our toolbar items to 10tap toolbar items
|
|
32
|
+
const TOOLBAR_ITEM_MAP: Record<ToolbarItem, string | null> = {
|
|
33
|
+
bold: 'bold',
|
|
34
|
+
italic: 'italic',
|
|
35
|
+
underline: 'underline',
|
|
36
|
+
strikethrough: 'strikethrough',
|
|
37
|
+
code: 'code',
|
|
38
|
+
heading1: 'h1',
|
|
39
|
+
heading2: 'h2',
|
|
40
|
+
heading3: 'h3',
|
|
41
|
+
bulletList: 'bulletList',
|
|
42
|
+
orderedList: 'orderedList',
|
|
43
|
+
taskList: 'taskList',
|
|
44
|
+
blockquote: 'blockquote',
|
|
45
|
+
codeBlock: 'codeBlock',
|
|
46
|
+
horizontalRule: 'horizontalRule',
|
|
47
|
+
link: 'link',
|
|
48
|
+
image: 'image',
|
|
49
|
+
undo: 'undo',
|
|
50
|
+
redo: 'redo',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Convert markdown to HTML using Showdown.
|
|
55
|
+
*/
|
|
56
|
+
function markdownToHtml(markdown: string): string {
|
|
57
|
+
return showdownConverter.makeHtml(markdown);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Convert HTML to markdown using Showdown.
|
|
62
|
+
*/
|
|
63
|
+
function htmlToMarkdown(html: string): string {
|
|
64
|
+
return showdownConverter.makeMarkdown(html);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Markdown editor for React Native using 10tap-editor.
|
|
69
|
+
*
|
|
70
|
+
* Uses a WebView-based Tiptap editor under the hood for rich text editing
|
|
71
|
+
* with markdown input/output support.
|
|
72
|
+
*/
|
|
73
|
+
const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>(
|
|
74
|
+
(
|
|
75
|
+
{
|
|
76
|
+
initialValue = '',
|
|
77
|
+
value,
|
|
78
|
+
onChange,
|
|
79
|
+
onFocus,
|
|
80
|
+
onBlur,
|
|
81
|
+
editable = true,
|
|
82
|
+
autoFocus = false,
|
|
83
|
+
placeholder,
|
|
84
|
+
toolbar = {},
|
|
85
|
+
size = 'md',
|
|
86
|
+
linkIntent = 'primary',
|
|
87
|
+
minHeight = 200,
|
|
88
|
+
maxHeight,
|
|
89
|
+
style,
|
|
90
|
+
testID,
|
|
91
|
+
id,
|
|
92
|
+
accessibilityLabel,
|
|
93
|
+
avoidIosKeyboard = true,
|
|
94
|
+
},
|
|
95
|
+
ref
|
|
96
|
+
) => {
|
|
97
|
+
const isControlled = value !== undefined;
|
|
98
|
+
const lastValueRef = useRef<string>(value ?? initialValue);
|
|
99
|
+
const editorRef = useRef<EditorBridge | null>(null);
|
|
100
|
+
|
|
101
|
+
// Apply style variants
|
|
102
|
+
editorStyles.useVariants({
|
|
103
|
+
size,
|
|
104
|
+
linkIntent,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Initialize editor with HTML content
|
|
108
|
+
const initialHtml = markdownToHtml(value ?? initialValue);
|
|
109
|
+
|
|
110
|
+
const editor = useEditorBridge({
|
|
111
|
+
autofocus: autoFocus,
|
|
112
|
+
avoidIosKeyboard,
|
|
113
|
+
initialContent: initialHtml,
|
|
114
|
+
editable,
|
|
115
|
+
onChange: async () => {
|
|
116
|
+
if (editorRef.current) {
|
|
117
|
+
try {
|
|
118
|
+
const html = await editorRef.current.getHTML();
|
|
119
|
+
const markdown = htmlToMarkdown(html);
|
|
120
|
+
if (markdown !== lastValueRef.current) {
|
|
121
|
+
lastValueRef.current = markdown;
|
|
122
|
+
onChange?.(markdown);
|
|
123
|
+
}
|
|
124
|
+
} catch {
|
|
125
|
+
// Editor might not be ready
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Store editor ref
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
editorRef.current = editor;
|
|
134
|
+
}, [editor]);
|
|
135
|
+
|
|
136
|
+
// Handle controlled value changes
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (isControlled && value !== lastValueRef.current && editorRef.current) {
|
|
139
|
+
const html = markdownToHtml(value);
|
|
140
|
+
editorRef.current.setContent(html);
|
|
141
|
+
lastValueRef.current = value;
|
|
142
|
+
}
|
|
143
|
+
}, [value, isControlled]);
|
|
144
|
+
|
|
145
|
+
// Expose ref methods
|
|
146
|
+
useImperativeHandle(
|
|
147
|
+
ref,
|
|
148
|
+
() => ({
|
|
149
|
+
getMarkdown: async () => {
|
|
150
|
+
if (!editorRef.current) return '';
|
|
151
|
+
try {
|
|
152
|
+
const html = await editorRef.current.getHTML();
|
|
153
|
+
return htmlToMarkdown(html);
|
|
154
|
+
} catch {
|
|
155
|
+
return lastValueRef.current;
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
setMarkdown: (markdown: string) => {
|
|
159
|
+
if (editorRef.current) {
|
|
160
|
+
const html = markdownToHtml(markdown);
|
|
161
|
+
editorRef.current.setContent(html);
|
|
162
|
+
lastValueRef.current = markdown;
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
focus: () => editorRef.current?.focus(),
|
|
166
|
+
blur: () => editorRef.current?.blur(),
|
|
167
|
+
isEmpty: async () => {
|
|
168
|
+
if (!editorRef.current) return true;
|
|
169
|
+
try {
|
|
170
|
+
const text = await editorRef.current.getText();
|
|
171
|
+
return !text || text.trim().length === 0;
|
|
172
|
+
} catch {
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
clear: () => {
|
|
177
|
+
editorRef.current?.setContent('');
|
|
178
|
+
lastValueRef.current = '';
|
|
179
|
+
},
|
|
180
|
+
undo: () => editorRef.current?.undo(),
|
|
181
|
+
redo: () => editorRef.current?.redo(),
|
|
182
|
+
}),
|
|
183
|
+
[]
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
// Map toolbar items
|
|
187
|
+
const toolbarItems = (toolbar.items ?? [
|
|
188
|
+
'bold',
|
|
189
|
+
'italic',
|
|
190
|
+
'underline',
|
|
191
|
+
'strikethrough',
|
|
192
|
+
'code',
|
|
193
|
+
'heading1',
|
|
194
|
+
'heading2',
|
|
195
|
+
'bulletList',
|
|
196
|
+
'orderedList',
|
|
197
|
+
'blockquote',
|
|
198
|
+
'codeBlock',
|
|
199
|
+
'link',
|
|
200
|
+
])
|
|
201
|
+
.map((item) => TOOLBAR_ITEM_MAP[item])
|
|
202
|
+
.filter((item): item is string => item !== null);
|
|
203
|
+
|
|
204
|
+
const showToolbar = toolbar.visible !== false && editable;
|
|
205
|
+
const toolbarPosition = toolbar.position ?? 'top';
|
|
206
|
+
|
|
207
|
+
const containerStyle = [
|
|
208
|
+
(editorStyles.container as any)({ size, linkIntent }),
|
|
209
|
+
style,
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
const editorContentStyle = [
|
|
213
|
+
(editorStyles.editorContent as any)({ size, linkIntent }),
|
|
214
|
+
{ minHeight },
|
|
215
|
+
maxHeight !== undefined && { maxHeight },
|
|
216
|
+
];
|
|
217
|
+
|
|
218
|
+
const content = (
|
|
219
|
+
<View
|
|
220
|
+
style={containerStyle}
|
|
221
|
+
nativeID={id}
|
|
222
|
+
testID={testID}
|
|
223
|
+
accessibilityLabel={accessibilityLabel}
|
|
224
|
+
>
|
|
225
|
+
{showToolbar && toolbarPosition === 'top' && (
|
|
226
|
+
<Toolbar editor={editor} items={DEFAULT_TOOLBAR_ITEMS} />
|
|
227
|
+
)}
|
|
228
|
+
<View style={editorContentStyle}>
|
|
229
|
+
<RichText
|
|
230
|
+
editor={editor}
|
|
231
|
+
onFocus={onFocus}
|
|
232
|
+
onBlur={onBlur}
|
|
233
|
+
style={nativeStyles.richText}
|
|
234
|
+
/>
|
|
235
|
+
</View>
|
|
236
|
+
{showToolbar && toolbarPosition === 'bottom' && (
|
|
237
|
+
<Toolbar editor={editor} items={DEFAULT_TOOLBAR_ITEMS} />
|
|
238
|
+
)}
|
|
239
|
+
</View>
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
if (Platform.OS === 'ios' && avoidIosKeyboard) {
|
|
243
|
+
return (
|
|
244
|
+
<KeyboardAvoidingView behavior="padding" style={{ flex: 1 }}>
|
|
245
|
+
{content}
|
|
246
|
+
</KeyboardAvoidingView>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return content;
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const nativeStyles = StyleSheet.create({
|
|
255
|
+
richText: {
|
|
256
|
+
flex: 1,
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
MarkdownEditor.displayName = 'MarkdownEditor';
|
|
261
|
+
|
|
262
|
+
export default MarkdownEditor;
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MarkdownEditor styles using defineStyle with theme integration.
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent styling for the markdown editor that
|
|
5
|
+
* integrates with the Idealyst theme system.
|
|
6
|
+
*/
|
|
7
|
+
import { StyleSheet } from 'react-native-unistyles';
|
|
8
|
+
import { defineStyle, ThemeStyleWrapper } from '@idealyst/theme';
|
|
9
|
+
import type { Theme as BaseTheme, Intent, Size } from '@idealyst/theme';
|
|
10
|
+
|
|
11
|
+
// Required: Unistyles must see StyleSheet usage in original source to process this file
|
|
12
|
+
void StyleSheet;
|
|
13
|
+
|
|
14
|
+
// Wrap theme for $iterator support
|
|
15
|
+
type Theme = ThemeStyleWrapper<BaseTheme>;
|
|
16
|
+
|
|
17
|
+
export type EditorVariants = {
|
|
18
|
+
size: Size;
|
|
19
|
+
linkIntent: Intent;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Dynamic props passed to editor style functions.
|
|
24
|
+
*/
|
|
25
|
+
export type EditorDynamicProps = {
|
|
26
|
+
linkIntent?: Intent;
|
|
27
|
+
size?: Size;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Editor styles with theme integration.
|
|
32
|
+
*/
|
|
33
|
+
// @ts-expect-error - MarkdownEditor is not in the ComponentName union yet
|
|
34
|
+
export const editorStyles = defineStyle('MarkdownEditor', (theme: Theme) => ({
|
|
35
|
+
// Main container
|
|
36
|
+
container: (_props: EditorDynamicProps) => ({
|
|
37
|
+
borderWidth: 1,
|
|
38
|
+
borderColor: theme.colors.border.primary,
|
|
39
|
+
borderRadius: theme.radii.md,
|
|
40
|
+
backgroundColor: theme.colors.surface.primary,
|
|
41
|
+
overflow: 'hidden',
|
|
42
|
+
}),
|
|
43
|
+
|
|
44
|
+
// Editor content area
|
|
45
|
+
editorContent: (_props: EditorDynamicProps) => ({
|
|
46
|
+
padding: 16,
|
|
47
|
+
minHeight: 200,
|
|
48
|
+
fontSize: theme.sizes.typography.body1.fontSize,
|
|
49
|
+
lineHeight: theme.sizes.typography.body1.lineHeight,
|
|
50
|
+
color: theme.colors.text.primary,
|
|
51
|
+
_web: {
|
|
52
|
+
outline: 'none',
|
|
53
|
+
// Tiptap editor styles
|
|
54
|
+
'& .tiptap': {
|
|
55
|
+
outline: 'none',
|
|
56
|
+
minHeight: '100%',
|
|
57
|
+
},
|
|
58
|
+
'& .tiptap p.is-editor-empty:first-child::before': {
|
|
59
|
+
content: 'attr(data-placeholder)',
|
|
60
|
+
color: theme.colors.text.tertiary,
|
|
61
|
+
pointerEvents: 'none',
|
|
62
|
+
float: 'left',
|
|
63
|
+
height: 0,
|
|
64
|
+
},
|
|
65
|
+
// Headings
|
|
66
|
+
'& .tiptap h1': {
|
|
67
|
+
fontSize: theme.sizes.typography.h1.fontSize,
|
|
68
|
+
lineHeight: theme.sizes.typography.h1.lineHeight,
|
|
69
|
+
fontWeight: '700',
|
|
70
|
+
marginTop: 24,
|
|
71
|
+
marginBottom: 16,
|
|
72
|
+
},
|
|
73
|
+
'& .tiptap h2': {
|
|
74
|
+
fontSize: theme.sizes.typography.h2.fontSize,
|
|
75
|
+
lineHeight: theme.sizes.typography.h2.lineHeight,
|
|
76
|
+
fontWeight: '600',
|
|
77
|
+
marginTop: 20,
|
|
78
|
+
marginBottom: 12,
|
|
79
|
+
borderBottom: `1px solid ${theme.colors.border.primary}`,
|
|
80
|
+
paddingBottom: 8,
|
|
81
|
+
},
|
|
82
|
+
'& .tiptap h3': {
|
|
83
|
+
fontSize: theme.sizes.typography.h3.fontSize,
|
|
84
|
+
lineHeight: theme.sizes.typography.h3.lineHeight,
|
|
85
|
+
fontWeight: '600',
|
|
86
|
+
marginTop: 16,
|
|
87
|
+
marginBottom: 8,
|
|
88
|
+
},
|
|
89
|
+
'& .tiptap h4, & .tiptap h5, & .tiptap h6': {
|
|
90
|
+
fontWeight: '600',
|
|
91
|
+
marginTop: 12,
|
|
92
|
+
marginBottom: 8,
|
|
93
|
+
},
|
|
94
|
+
// Paragraphs
|
|
95
|
+
'& .tiptap p': {
|
|
96
|
+
marginVertical: 8,
|
|
97
|
+
},
|
|
98
|
+
// Links
|
|
99
|
+
'& .tiptap a': {
|
|
100
|
+
color: theme.intents?.primary?.primary ?? theme.colors.text.primary,
|
|
101
|
+
textDecoration: 'underline',
|
|
102
|
+
cursor: 'pointer',
|
|
103
|
+
},
|
|
104
|
+
// Code
|
|
105
|
+
'& .tiptap code': {
|
|
106
|
+
fontFamily: 'monospace',
|
|
107
|
+
backgroundColor: theme.colors.surface.secondary,
|
|
108
|
+
paddingHorizontal: 6,
|
|
109
|
+
paddingVertical: 2,
|
|
110
|
+
borderRadius: theme.radii.xs,
|
|
111
|
+
fontSize: theme.sizes.typography.caption.fontSize,
|
|
112
|
+
},
|
|
113
|
+
'& .tiptap pre': {
|
|
114
|
+
fontFamily: 'monospace',
|
|
115
|
+
backgroundColor: theme.colors.surface.secondary,
|
|
116
|
+
padding: 16,
|
|
117
|
+
borderRadius: theme.radii.md,
|
|
118
|
+
marginVertical: 12,
|
|
119
|
+
overflow: 'auto',
|
|
120
|
+
},
|
|
121
|
+
'& .tiptap pre code': {
|
|
122
|
+
backgroundColor: 'transparent',
|
|
123
|
+
padding: 0,
|
|
124
|
+
},
|
|
125
|
+
// Blockquote
|
|
126
|
+
'& .tiptap blockquote': {
|
|
127
|
+
borderLeft: `4px solid ${theme.colors.border.secondary}`,
|
|
128
|
+
paddingLeft: 16,
|
|
129
|
+
paddingVertical: 8,
|
|
130
|
+
marginVertical: 12,
|
|
131
|
+
backgroundColor: theme.colors.surface.secondary,
|
|
132
|
+
borderRadius: theme.radii.sm,
|
|
133
|
+
fontStyle: 'italic',
|
|
134
|
+
color: theme.colors.text.secondary,
|
|
135
|
+
},
|
|
136
|
+
// Lists
|
|
137
|
+
'& .tiptap ul, & .tiptap ol': {
|
|
138
|
+
marginVertical: 8,
|
|
139
|
+
paddingLeft: 24,
|
|
140
|
+
},
|
|
141
|
+
'& .tiptap li': {
|
|
142
|
+
marginVertical: 4,
|
|
143
|
+
},
|
|
144
|
+
// Task list
|
|
145
|
+
'& .tiptap ul[data-type="taskList"]': {
|
|
146
|
+
listStyle: 'none',
|
|
147
|
+
padding: 0,
|
|
148
|
+
},
|
|
149
|
+
'& .tiptap ul[data-type="taskList"] li': {
|
|
150
|
+
display: 'flex',
|
|
151
|
+
alignItems: 'flex-start',
|
|
152
|
+
gap: 8,
|
|
153
|
+
},
|
|
154
|
+
'& .tiptap ul[data-type="taskList"] li input': {
|
|
155
|
+
marginTop: 4,
|
|
156
|
+
},
|
|
157
|
+
// Horizontal rule
|
|
158
|
+
'& .tiptap hr': {
|
|
159
|
+
height: 1,
|
|
160
|
+
backgroundColor: theme.colors.border.secondary,
|
|
161
|
+
marginVertical: 24,
|
|
162
|
+
border: 'none',
|
|
163
|
+
},
|
|
164
|
+
// Table
|
|
165
|
+
'& .tiptap table': {
|
|
166
|
+
borderCollapse: 'collapse',
|
|
167
|
+
width: '100%',
|
|
168
|
+
marginVertical: 12,
|
|
169
|
+
},
|
|
170
|
+
'& .tiptap th, & .tiptap td': {
|
|
171
|
+
border: `1px solid ${theme.colors.border.primary}`,
|
|
172
|
+
padding: 12,
|
|
173
|
+
},
|
|
174
|
+
'& .tiptap th': {
|
|
175
|
+
backgroundColor: theme.colors.surface.secondary,
|
|
176
|
+
fontWeight: '600',
|
|
177
|
+
textAlign: 'left',
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
}),
|
|
181
|
+
|
|
182
|
+
// Toolbar container
|
|
183
|
+
toolbar: (_props: EditorDynamicProps) => ({
|
|
184
|
+
flexDirection: 'row',
|
|
185
|
+
flexWrap: 'wrap',
|
|
186
|
+
gap: 4,
|
|
187
|
+
padding: 8,
|
|
188
|
+
borderBottomWidth: 1,
|
|
189
|
+
borderBottomColor: theme.colors.border.primary,
|
|
190
|
+
backgroundColor: theme.colors.surface.secondary,
|
|
191
|
+
}),
|
|
192
|
+
|
|
193
|
+
// Toolbar button
|
|
194
|
+
toolbarButton: (_props: EditorDynamicProps) => ({
|
|
195
|
+
paddingHorizontal: 10,
|
|
196
|
+
paddingVertical: 6,
|
|
197
|
+
borderRadius: theme.radii.sm,
|
|
198
|
+
backgroundColor: 'transparent',
|
|
199
|
+
borderWidth: 0,
|
|
200
|
+
color: theme.colors.text.primary,
|
|
201
|
+
fontSize: 14,
|
|
202
|
+
fontWeight: '500',
|
|
203
|
+
_web: {
|
|
204
|
+
cursor: 'pointer',
|
|
205
|
+
transition: 'background-color 0.15s ease',
|
|
206
|
+
_hover: {
|
|
207
|
+
backgroundColor: theme.colors.surface.tertiary,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
211
|
+
|
|
212
|
+
// Toolbar button active state
|
|
213
|
+
toolbarButtonActive: (_props: EditorDynamicProps) => ({
|
|
214
|
+
backgroundColor: theme.colors.surface.tertiary,
|
|
215
|
+
color: theme.intents?.primary?.primary ?? theme.colors.text.primary,
|
|
216
|
+
}),
|
|
217
|
+
|
|
218
|
+
// Focus ring for accessibility
|
|
219
|
+
focusRing: (_props: EditorDynamicProps) => ({
|
|
220
|
+
_web: {
|
|
221
|
+
_focus: {
|
|
222
|
+
outline: `2px solid ${theme.intents?.primary?.primary ?? theme.colors.border.primary}`,
|
|
223
|
+
outlineOffset: 2,
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
}),
|
|
227
|
+
}));
|