@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.
@@ -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';