@idealyst/markdown 1.2.38 → 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
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useImperativeHandle,
|
|
4
|
+
useEffect,
|
|
5
|
+
useCallback,
|
|
6
|
+
useRef,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { useEditor, EditorContent } from '@tiptap/react';
|
|
9
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
10
|
+
import Placeholder from '@tiptap/extension-placeholder';
|
|
11
|
+
import Link from '@tiptap/extension-link';
|
|
12
|
+
import TaskList from '@tiptap/extension-task-list';
|
|
13
|
+
import TaskItem from '@tiptap/extension-task-item';
|
|
14
|
+
import Underline from '@tiptap/extension-underline';
|
|
15
|
+
import { Markdown as TiptapMarkdown } from 'tiptap-markdown';
|
|
16
|
+
import { getWebProps } from 'react-native-unistyles/web';
|
|
17
|
+
import { editorStyles } from './MarkdownEditor.styles';
|
|
18
|
+
import { EditorToolbar } from './EditorToolbar.web';
|
|
19
|
+
import type { MarkdownEditorProps, MarkdownEditorRef } from './types';
|
|
20
|
+
|
|
21
|
+
const DEFAULT_TOOLBAR_ITEMS = [
|
|
22
|
+
'bold',
|
|
23
|
+
'italic',
|
|
24
|
+
'underline',
|
|
25
|
+
'strikethrough',
|
|
26
|
+
'code',
|
|
27
|
+
'heading1',
|
|
28
|
+
'heading2',
|
|
29
|
+
'bulletList',
|
|
30
|
+
'orderedList',
|
|
31
|
+
'blockquote',
|
|
32
|
+
'codeBlock',
|
|
33
|
+
'link',
|
|
34
|
+
] as const;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Markdown editor for web using Tiptap.
|
|
38
|
+
*
|
|
39
|
+
* Provides a rich text editing experience with markdown input/output.
|
|
40
|
+
* Uses Tiptap with the tiptap-markdown extension for seamless markdown handling.
|
|
41
|
+
*/
|
|
42
|
+
const MarkdownEditor = forwardRef<MarkdownEditorRef, MarkdownEditorProps>(
|
|
43
|
+
(
|
|
44
|
+
{
|
|
45
|
+
initialValue = '',
|
|
46
|
+
value,
|
|
47
|
+
onChange,
|
|
48
|
+
onFocus,
|
|
49
|
+
onBlur,
|
|
50
|
+
editable = true,
|
|
51
|
+
autoFocus = false,
|
|
52
|
+
placeholder,
|
|
53
|
+
toolbar = {},
|
|
54
|
+
size = 'md',
|
|
55
|
+
linkIntent = 'primary',
|
|
56
|
+
minHeight,
|
|
57
|
+
maxHeight,
|
|
58
|
+
style,
|
|
59
|
+
testID,
|
|
60
|
+
id,
|
|
61
|
+
accessibilityLabel,
|
|
62
|
+
},
|
|
63
|
+
ref
|
|
64
|
+
) => {
|
|
65
|
+
const isControlled = value !== undefined;
|
|
66
|
+
const lastValueRef = useRef<string>(value ?? initialValue);
|
|
67
|
+
|
|
68
|
+
// Apply style variants
|
|
69
|
+
editorStyles.useVariants({
|
|
70
|
+
size,
|
|
71
|
+
linkIntent,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const editor = useEditor({
|
|
75
|
+
extensions: [
|
|
76
|
+
StarterKit.configure({
|
|
77
|
+
heading: {
|
|
78
|
+
levels: [1, 2, 3, 4, 5, 6],
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
Placeholder.configure({
|
|
82
|
+
placeholder: placeholder ?? 'Start writing...',
|
|
83
|
+
}),
|
|
84
|
+
Link.configure({
|
|
85
|
+
openOnClick: false,
|
|
86
|
+
HTMLAttributes: {
|
|
87
|
+
rel: 'noopener noreferrer',
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
TaskList,
|
|
91
|
+
TaskItem.configure({
|
|
92
|
+
nested: true,
|
|
93
|
+
}),
|
|
94
|
+
Underline,
|
|
95
|
+
TiptapMarkdown.configure({
|
|
96
|
+
html: false,
|
|
97
|
+
transformPastedText: true,
|
|
98
|
+
transformCopiedText: true,
|
|
99
|
+
}),
|
|
100
|
+
],
|
|
101
|
+
content: value ?? initialValue,
|
|
102
|
+
editable,
|
|
103
|
+
autofocus: autoFocus,
|
|
104
|
+
onUpdate: ({ editor: ed }) => {
|
|
105
|
+
const markdown = ed.storage.markdown.getMarkdown();
|
|
106
|
+
lastValueRef.current = markdown;
|
|
107
|
+
onChange?.(markdown);
|
|
108
|
+
},
|
|
109
|
+
onFocus: () => onFocus?.(),
|
|
110
|
+
onBlur: () => onBlur?.(),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Handle controlled value changes
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (isControlled && editor && value !== lastValueRef.current) {
|
|
116
|
+
const { from, to } = editor.state.selection;
|
|
117
|
+
editor.commands.setContent(value, false, {
|
|
118
|
+
preserveWhitespace: 'full',
|
|
119
|
+
});
|
|
120
|
+
// Try to restore cursor position
|
|
121
|
+
const newDocLength = editor.state.doc.content.size;
|
|
122
|
+
const safeFrom = Math.min(from, newDocLength);
|
|
123
|
+
const safeTo = Math.min(to, newDocLength);
|
|
124
|
+
editor.commands.setTextSelection({ from: safeFrom, to: safeTo });
|
|
125
|
+
lastValueRef.current = value;
|
|
126
|
+
}
|
|
127
|
+
}, [value, isControlled, editor]);
|
|
128
|
+
|
|
129
|
+
// Handle editable changes
|
|
130
|
+
useEffect(() => {
|
|
131
|
+
if (editor) {
|
|
132
|
+
editor.setEditable(editable);
|
|
133
|
+
}
|
|
134
|
+
}, [editable, editor]);
|
|
135
|
+
|
|
136
|
+
// Expose ref methods
|
|
137
|
+
useImperativeHandle(
|
|
138
|
+
ref,
|
|
139
|
+
() => ({
|
|
140
|
+
getMarkdown: async () => {
|
|
141
|
+
if (!editor) return '';
|
|
142
|
+
return editor.storage.markdown.getMarkdown();
|
|
143
|
+
},
|
|
144
|
+
setMarkdown: (markdown: string) => {
|
|
145
|
+
if (editor) {
|
|
146
|
+
editor.commands.setContent(markdown, false, {
|
|
147
|
+
preserveWhitespace: 'full',
|
|
148
|
+
});
|
|
149
|
+
lastValueRef.current = markdown;
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
focus: () => editor?.commands.focus(),
|
|
153
|
+
blur: () => editor?.commands.blur(),
|
|
154
|
+
isEmpty: async () => editor?.isEmpty ?? true,
|
|
155
|
+
clear: () => {
|
|
156
|
+
editor?.commands.clearContent();
|
|
157
|
+
lastValueRef.current = '';
|
|
158
|
+
},
|
|
159
|
+
undo: () => editor?.commands.undo(),
|
|
160
|
+
redo: () => editor?.commands.redo(),
|
|
161
|
+
}),
|
|
162
|
+
[editor]
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const handleToolbarAction = useCallback(
|
|
166
|
+
(action: string) => {
|
|
167
|
+
if (!editor) return;
|
|
168
|
+
|
|
169
|
+
switch (action) {
|
|
170
|
+
case 'bold':
|
|
171
|
+
editor.chain().focus().toggleBold().run();
|
|
172
|
+
break;
|
|
173
|
+
case 'italic':
|
|
174
|
+
editor.chain().focus().toggleItalic().run();
|
|
175
|
+
break;
|
|
176
|
+
case 'underline':
|
|
177
|
+
editor.chain().focus().toggleUnderline().run();
|
|
178
|
+
break;
|
|
179
|
+
case 'strikethrough':
|
|
180
|
+
editor.chain().focus().toggleStrike().run();
|
|
181
|
+
break;
|
|
182
|
+
case 'code':
|
|
183
|
+
editor.chain().focus().toggleCode().run();
|
|
184
|
+
break;
|
|
185
|
+
case 'heading1':
|
|
186
|
+
editor.chain().focus().toggleHeading({ level: 1 }).run();
|
|
187
|
+
break;
|
|
188
|
+
case 'heading2':
|
|
189
|
+
editor.chain().focus().toggleHeading({ level: 2 }).run();
|
|
190
|
+
break;
|
|
191
|
+
case 'heading3':
|
|
192
|
+
editor.chain().focus().toggleHeading({ level: 3 }).run();
|
|
193
|
+
break;
|
|
194
|
+
case 'bulletList':
|
|
195
|
+
editor.chain().focus().toggleBulletList().run();
|
|
196
|
+
break;
|
|
197
|
+
case 'orderedList':
|
|
198
|
+
editor.chain().focus().toggleOrderedList().run();
|
|
199
|
+
break;
|
|
200
|
+
case 'taskList':
|
|
201
|
+
editor.chain().focus().toggleTaskList().run();
|
|
202
|
+
break;
|
|
203
|
+
case 'blockquote':
|
|
204
|
+
editor.chain().focus().toggleBlockquote().run();
|
|
205
|
+
break;
|
|
206
|
+
case 'codeBlock':
|
|
207
|
+
editor.chain().focus().toggleCodeBlock().run();
|
|
208
|
+
break;
|
|
209
|
+
case 'horizontalRule':
|
|
210
|
+
editor.chain().focus().setHorizontalRule().run();
|
|
211
|
+
break;
|
|
212
|
+
case 'link': {
|
|
213
|
+
const previousUrl = editor.getAttributes('link').href;
|
|
214
|
+
const url = window.prompt('URL', previousUrl);
|
|
215
|
+
if (url === null) return;
|
|
216
|
+
if (url === '') {
|
|
217
|
+
editor.chain().focus().extendMarkRange('link').unsetLink().run();
|
|
218
|
+
} else {
|
|
219
|
+
editor
|
|
220
|
+
.chain()
|
|
221
|
+
.focus()
|
|
222
|
+
.extendMarkRange('link')
|
|
223
|
+
.setLink({ href: url })
|
|
224
|
+
.run();
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
case 'undo':
|
|
229
|
+
editor.chain().focus().undo().run();
|
|
230
|
+
break;
|
|
231
|
+
case 'redo':
|
|
232
|
+
editor.chain().focus().redo().run();
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
[editor]
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
const isActive = useCallback(
|
|
240
|
+
(action: string): boolean => {
|
|
241
|
+
if (!editor) return false;
|
|
242
|
+
|
|
243
|
+
switch (action) {
|
|
244
|
+
case 'bold':
|
|
245
|
+
return editor.isActive('bold');
|
|
246
|
+
case 'italic':
|
|
247
|
+
return editor.isActive('italic');
|
|
248
|
+
case 'underline':
|
|
249
|
+
return editor.isActive('underline');
|
|
250
|
+
case 'strikethrough':
|
|
251
|
+
return editor.isActive('strike');
|
|
252
|
+
case 'code':
|
|
253
|
+
return editor.isActive('code');
|
|
254
|
+
case 'heading1':
|
|
255
|
+
return editor.isActive('heading', { level: 1 });
|
|
256
|
+
case 'heading2':
|
|
257
|
+
return editor.isActive('heading', { level: 2 });
|
|
258
|
+
case 'heading3':
|
|
259
|
+
return editor.isActive('heading', { level: 3 });
|
|
260
|
+
case 'bulletList':
|
|
261
|
+
return editor.isActive('bulletList');
|
|
262
|
+
case 'orderedList':
|
|
263
|
+
return editor.isActive('orderedList');
|
|
264
|
+
case 'taskList':
|
|
265
|
+
return editor.isActive('taskList');
|
|
266
|
+
case 'blockquote':
|
|
267
|
+
return editor.isActive('blockquote');
|
|
268
|
+
case 'codeBlock':
|
|
269
|
+
return editor.isActive('codeBlock');
|
|
270
|
+
case 'link':
|
|
271
|
+
return editor.isActive('link');
|
|
272
|
+
default:
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
[editor]
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Get container styles
|
|
280
|
+
const containerStyleArray = [
|
|
281
|
+
(editorStyles.container as any)({ size, linkIntent }),
|
|
282
|
+
minHeight !== undefined && { minHeight },
|
|
283
|
+
maxHeight !== undefined && { maxHeight, overflow: 'auto' },
|
|
284
|
+
style,
|
|
285
|
+
].filter(Boolean);
|
|
286
|
+
|
|
287
|
+
const webProps = getWebProps(containerStyleArray);
|
|
288
|
+
const toolbarItems = toolbar.items ?? DEFAULT_TOOLBAR_ITEMS;
|
|
289
|
+
const showToolbar = toolbar.visible !== false && editable;
|
|
290
|
+
const toolbarPosition = toolbar.position ?? 'top';
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<div
|
|
294
|
+
{...webProps}
|
|
295
|
+
id={id}
|
|
296
|
+
data-testid={testID}
|
|
297
|
+
aria-label={accessibilityLabel}
|
|
298
|
+
>
|
|
299
|
+
{showToolbar && toolbarPosition === 'top' && (
|
|
300
|
+
<EditorToolbar
|
|
301
|
+
items={toolbarItems}
|
|
302
|
+
onAction={handleToolbarAction}
|
|
303
|
+
isActive={isActive}
|
|
304
|
+
size={size}
|
|
305
|
+
linkIntent={linkIntent}
|
|
306
|
+
/>
|
|
307
|
+
)}
|
|
308
|
+
<div
|
|
309
|
+
{...getWebProps([
|
|
310
|
+
(editorStyles.editorContent as any)({ size, linkIntent }),
|
|
311
|
+
])}
|
|
312
|
+
>
|
|
313
|
+
<EditorContent editor={editor} />
|
|
314
|
+
</div>
|
|
315
|
+
{showToolbar && toolbarPosition === 'bottom' && (
|
|
316
|
+
<EditorToolbar
|
|
317
|
+
items={toolbarItems}
|
|
318
|
+
onAction={handleToolbarAction}
|
|
319
|
+
isActive={isActive}
|
|
320
|
+
size={size}
|
|
321
|
+
linkIntent={linkIntent}
|
|
322
|
+
/>
|
|
323
|
+
)}
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
MarkdownEditor.displayName = 'MarkdownEditor';
|
|
330
|
+
|
|
331
|
+
export default MarkdownEditor;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { useState, useRef } from 'react';
|
|
2
|
+
import { View, Text, Card, Button, Screen } from '@idealyst/components';
|
|
3
|
+
import { MarkdownEditor } from '../index';
|
|
4
|
+
import type { MarkdownEditorRef } from '../types';
|
|
5
|
+
|
|
6
|
+
const SAMPLE_MARKDOWN = `# Welcome to the Editor
|
|
7
|
+
|
|
8
|
+
This is a **rich text** editor that works with *markdown*.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- Bold, italic, underline formatting
|
|
13
|
+
- Headings (H1-H6)
|
|
14
|
+
- Bullet and numbered lists
|
|
15
|
+
- Task lists
|
|
16
|
+
- Code blocks
|
|
17
|
+
- Blockquotes
|
|
18
|
+
- Links
|
|
19
|
+
|
|
20
|
+
### Code Example
|
|
21
|
+
|
|
22
|
+
\`\`\`typescript
|
|
23
|
+
const greeting = "Hello, World!";
|
|
24
|
+
console.log(greeting);
|
|
25
|
+
\`\`\`
|
|
26
|
+
|
|
27
|
+
> This is a blockquote with some important information.
|
|
28
|
+
|
|
29
|
+
Try editing this content!
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Basic usage example
|
|
34
|
+
*/
|
|
35
|
+
function BasicEditorExample() {
|
|
36
|
+
const [content, setContent] = useState(SAMPLE_MARKDOWN);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Card>
|
|
40
|
+
<View spacing="md" style={{ padding: 16 }}>
|
|
41
|
+
<Text size="lg" weight="semibold">Basic Editor</Text>
|
|
42
|
+
<Text size="sm" color="secondary">
|
|
43
|
+
A simple controlled editor with markdown input/output
|
|
44
|
+
</Text>
|
|
45
|
+
<MarkdownEditor
|
|
46
|
+
value={content}
|
|
47
|
+
onChange={setContent}
|
|
48
|
+
placeholder="Start writing..."
|
|
49
|
+
minHeight={300}
|
|
50
|
+
/>
|
|
51
|
+
</View>
|
|
52
|
+
</Card>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Controlled editor with ref methods
|
|
58
|
+
*/
|
|
59
|
+
function ControlledEditorExample() {
|
|
60
|
+
const [content, setContent] = useState('');
|
|
61
|
+
const [output, setOutput] = useState('');
|
|
62
|
+
const editorRef = useRef<MarkdownEditorRef>(null);
|
|
63
|
+
|
|
64
|
+
const handleClear = () => {
|
|
65
|
+
editorRef.current?.clear();
|
|
66
|
+
setOutput('');
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const handleGetContent = async () => {
|
|
70
|
+
const markdown = await editorRef.current?.getMarkdown();
|
|
71
|
+
setOutput(markdown ?? '');
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const handleSetContent = () => {
|
|
75
|
+
editorRef.current?.setMarkdown('# New Content\n\nThis was set programmatically.');
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<Card>
|
|
80
|
+
<View spacing="md" style={{ padding: 16 }}>
|
|
81
|
+
<Text size="lg" weight="semibold">Editor with Ref Methods</Text>
|
|
82
|
+
<Text size="sm" color="secondary">
|
|
83
|
+
Use ref methods to programmatically control the editor
|
|
84
|
+
</Text>
|
|
85
|
+
<View direction="row" spacing="sm">
|
|
86
|
+
<Button size="sm" type="outlined" onPress={handleClear}>
|
|
87
|
+
Clear
|
|
88
|
+
</Button>
|
|
89
|
+
<Button size="sm" type="outlined" onPress={handleGetContent}>
|
|
90
|
+
Get Markdown
|
|
91
|
+
</Button>
|
|
92
|
+
<Button size="sm" type="outlined" onPress={handleSetContent}>
|
|
93
|
+
Set Content
|
|
94
|
+
</Button>
|
|
95
|
+
</View>
|
|
96
|
+
<MarkdownEditor
|
|
97
|
+
ref={editorRef}
|
|
98
|
+
value={content}
|
|
99
|
+
onChange={setContent}
|
|
100
|
+
placeholder="Type something..."
|
|
101
|
+
minHeight={200}
|
|
102
|
+
/>
|
|
103
|
+
{output ? (
|
|
104
|
+
<View spacing="sm">
|
|
105
|
+
<Text size="sm" weight="semibold">Output:</Text>
|
|
106
|
+
<View style={{ backgroundColor: '#f5f5f5', padding: 12, borderRadius: 8 }}>
|
|
107
|
+
<Text size="sm" style={{ fontFamily: 'monospace' }}>
|
|
108
|
+
{output}
|
|
109
|
+
</Text>
|
|
110
|
+
</View>
|
|
111
|
+
</View>
|
|
112
|
+
) : null}
|
|
113
|
+
</View>
|
|
114
|
+
</Card>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Read-only mode example
|
|
120
|
+
*/
|
|
121
|
+
function ReadOnlyEditorExample() {
|
|
122
|
+
return (
|
|
123
|
+
<Card>
|
|
124
|
+
<View spacing="md" style={{ padding: 16 }}>
|
|
125
|
+
<Text size="lg" weight="semibold">Read-Only Mode</Text>
|
|
126
|
+
<Text size="sm" color="secondary">
|
|
127
|
+
Use editable=false for a rich markdown viewer
|
|
128
|
+
</Text>
|
|
129
|
+
<MarkdownEditor
|
|
130
|
+
initialValue={SAMPLE_MARKDOWN}
|
|
131
|
+
editable={false}
|
|
132
|
+
toolbar={{ visible: false }}
|
|
133
|
+
minHeight={250}
|
|
134
|
+
/>
|
|
135
|
+
</View>
|
|
136
|
+
</Card>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Custom toolbar configuration
|
|
142
|
+
*/
|
|
143
|
+
function CustomToolbarExample() {
|
|
144
|
+
const [content, setContent] = useState('');
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<Card>
|
|
148
|
+
<View spacing="md" style={{ padding: 16 }}>
|
|
149
|
+
<Text size="lg" weight="semibold">Custom Toolbar</Text>
|
|
150
|
+
<Text size="sm" color="secondary">
|
|
151
|
+
Configure which toolbar items appear and their position
|
|
152
|
+
</Text>
|
|
153
|
+
<MarkdownEditor
|
|
154
|
+
value={content}
|
|
155
|
+
onChange={setContent}
|
|
156
|
+
placeholder="Minimal formatting options..."
|
|
157
|
+
toolbar={{
|
|
158
|
+
items: ['bold', 'italic', 'link', 'bulletList', 'orderedList'],
|
|
159
|
+
position: 'bottom',
|
|
160
|
+
}}
|
|
161
|
+
minHeight={200}
|
|
162
|
+
/>
|
|
163
|
+
</View>
|
|
164
|
+
</Card>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Editor with all examples combined
|
|
170
|
+
*/
|
|
171
|
+
export function MarkdownEditorExamples() {
|
|
172
|
+
return (
|
|
173
|
+
<Screen>
|
|
174
|
+
<View spacing="lg" padding={12}>
|
|
175
|
+
<Text size="xl" weight="bold">
|
|
176
|
+
Markdown Editor
|
|
177
|
+
</Text>
|
|
178
|
+
<Text color="secondary">
|
|
179
|
+
A cross-platform WYSIWYG markdown editor. Uses Tiptap on web and 10tap-editor on native.
|
|
180
|
+
</Text>
|
|
181
|
+
|
|
182
|
+
<Card>
|
|
183
|
+
<View spacing="sm" style={{ padding: 16 }}>
|
|
184
|
+
<Text size="lg" weight="semibold">Features</Text>
|
|
185
|
+
<View spacing="xs">
|
|
186
|
+
<Text size="sm">• Markdown input/output - no HTML exposed to consumers</Text>
|
|
187
|
+
<Text size="sm">• Rich text editing with formatting toolbar</Text>
|
|
188
|
+
<Text size="sm">• Controlled and uncontrolled modes</Text>
|
|
189
|
+
<Text size="sm">• Ref methods for programmatic control</Text>
|
|
190
|
+
<Text size="sm">• Configurable toolbar items and position</Text>
|
|
191
|
+
<Text size="sm">• Theme integration via Unistyles</Text>
|
|
192
|
+
</View>
|
|
193
|
+
</View>
|
|
194
|
+
</Card>
|
|
195
|
+
|
|
196
|
+
<BasicEditorExample />
|
|
197
|
+
<ControlledEditorExample />
|
|
198
|
+
<ReadOnlyEditorExample />
|
|
199
|
+
<CustomToolbarExample />
|
|
200
|
+
</View>
|
|
201
|
+
</Screen>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export default MarkdownEditorExamples;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MarkdownEditorExamples } from './MarkdownEditorExamples';
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @idealyst/markdown/editor - Cross-platform markdown editor (React Native)
|
|
3
|
+
*
|
|
4
|
+
* Provides a MarkdownEditor component for editing markdown content
|
|
5
|
+
* with a rich text interface on React Native using 10tap-editor.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Main component
|
|
9
|
+
export { default as MarkdownEditor } from './MarkdownEditor.native';
|
|
10
|
+
|
|
11
|
+
// Types
|
|
12
|
+
export type {
|
|
13
|
+
MarkdownEditorProps,
|
|
14
|
+
MarkdownEditorRef,
|
|
15
|
+
ToolbarItem,
|
|
16
|
+
ToolbarConfig,
|
|
17
|
+
} from './types';
|
|
18
|
+
|
|
19
|
+
// Style types
|
|
20
|
+
export type {
|
|
21
|
+
EditorDynamicProps,
|
|
22
|
+
EditorVariants,
|
|
23
|
+
} from './MarkdownEditor.styles';
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @idealyst/markdown/editor - Cross-platform markdown editor
|
|
3
|
+
*
|
|
4
|
+
* Provides a MarkdownEditor component for editing markdown content
|
|
5
|
+
* with a rich text interface on both web and React Native platforms.
|
|
6
|
+
*
|
|
7
|
+
* Web uses Tiptap with tiptap-markdown extension.
|
|
8
|
+
* Native uses 10tap-editor (Tiptap in WebView).
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```tsx
|
|
12
|
+
* import { MarkdownEditor } from '@idealyst/markdown/editor';
|
|
13
|
+
*
|
|
14
|
+
* function App() {
|
|
15
|
+
* const [content, setContent] = useState('# Hello World');
|
|
16
|
+
*
|
|
17
|
+
* return (
|
|
18
|
+
* <MarkdownEditor
|
|
19
|
+
* value={content}
|
|
20
|
+
* onChange={setContent}
|
|
21
|
+
* placeholder="Start writing..."
|
|
22
|
+
* />
|
|
23
|
+
* );
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
// Main component
|
|
29
|
+
export { default as MarkdownEditor } from './MarkdownEditor.web';
|
|
30
|
+
|
|
31
|
+
// Types
|
|
32
|
+
export type {
|
|
33
|
+
MarkdownEditorProps,
|
|
34
|
+
MarkdownEditorRef,
|
|
35
|
+
ToolbarItem,
|
|
36
|
+
ToolbarConfig,
|
|
37
|
+
} from './types';
|
|
38
|
+
|
|
39
|
+
// Style types
|
|
40
|
+
export type {
|
|
41
|
+
EditorDynamicProps,
|
|
42
|
+
EditorVariants,
|
|
43
|
+
} from './MarkdownEditor.styles';
|