@furystack/shades-common-components 12.2.0 → 12.4.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.
Files changed (100) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/esm/components/form.d.ts +5 -2
  3. package/esm/components/form.d.ts.map +1 -1
  4. package/esm/components/form.js +28 -6
  5. package/esm/components/form.js.map +1 -1
  6. package/esm/components/form.spec.js +207 -0
  7. package/esm/components/form.spec.js.map +1 -1
  8. package/esm/components/index.d.ts +1 -0
  9. package/esm/components/index.d.ts.map +1 -1
  10. package/esm/components/index.js +1 -0
  11. package/esm/components/index.js.map +1 -1
  12. package/esm/components/markdown/index.d.ts +5 -0
  13. package/esm/components/markdown/index.d.ts.map +1 -0
  14. package/esm/components/markdown/index.js +5 -0
  15. package/esm/components/markdown/index.js.map +1 -0
  16. package/esm/components/markdown/markdown-display.d.ts +19 -0
  17. package/esm/components/markdown/markdown-display.d.ts.map +1 -0
  18. package/esm/components/markdown/markdown-display.js +149 -0
  19. package/esm/components/markdown/markdown-display.js.map +1 -0
  20. package/esm/components/markdown/markdown-display.spec.d.ts +2 -0
  21. package/esm/components/markdown/markdown-display.spec.d.ts.map +1 -0
  22. package/esm/components/markdown/markdown-display.spec.js +191 -0
  23. package/esm/components/markdown/markdown-display.spec.js.map +1 -0
  24. package/esm/components/markdown/markdown-editor.d.ts +25 -0
  25. package/esm/components/markdown/markdown-editor.d.ts.map +1 -0
  26. package/esm/components/markdown/markdown-editor.js +113 -0
  27. package/esm/components/markdown/markdown-editor.js.map +1 -0
  28. package/esm/components/markdown/markdown-editor.spec.d.ts +2 -0
  29. package/esm/components/markdown/markdown-editor.spec.d.ts.map +1 -0
  30. package/esm/components/markdown/markdown-editor.spec.js +111 -0
  31. package/esm/components/markdown/markdown-editor.spec.js.map +1 -0
  32. package/esm/components/markdown/markdown-input.d.ts +29 -0
  33. package/esm/components/markdown/markdown-input.d.ts.map +1 -0
  34. package/esm/components/markdown/markdown-input.js +100 -0
  35. package/esm/components/markdown/markdown-input.js.map +1 -0
  36. package/esm/components/markdown/markdown-input.spec.d.ts +2 -0
  37. package/esm/components/markdown/markdown-input.spec.d.ts.map +1 -0
  38. package/esm/components/markdown/markdown-input.spec.js +215 -0
  39. package/esm/components/markdown/markdown-input.spec.js.map +1 -0
  40. package/esm/components/markdown/markdown-parser.d.ts +82 -0
  41. package/esm/components/markdown/markdown-parser.d.ts.map +1 -0
  42. package/esm/components/markdown/markdown-parser.js +274 -0
  43. package/esm/components/markdown/markdown-parser.js.map +1 -0
  44. package/esm/components/markdown/markdown-parser.spec.d.ts +2 -0
  45. package/esm/components/markdown/markdown-parser.spec.d.ts.map +1 -0
  46. package/esm/components/markdown/markdown-parser.spec.js +229 -0
  47. package/esm/components/markdown/markdown-parser.spec.js.map +1 -0
  48. package/esm/components/styles.d.ts +1 -0
  49. package/esm/components/styles.d.ts.map +1 -1
  50. package/esm/components/styles.js.map +1 -1
  51. package/esm/components/typography.d.ts.map +1 -1
  52. package/esm/components/typography.js +26 -14
  53. package/esm/components/typography.js.map +1 -1
  54. package/esm/services/css-variable-theme.d.ts +3 -0
  55. package/esm/services/css-variable-theme.d.ts.map +1 -1
  56. package/esm/services/css-variable-theme.js +3 -0
  57. package/esm/services/css-variable-theme.js.map +1 -1
  58. package/esm/services/css-variable-theme.spec.js +3 -0
  59. package/esm/services/css-variable-theme.spec.js.map +1 -1
  60. package/esm/services/default-dark-palette.d.ts +8 -0
  61. package/esm/services/default-dark-palette.d.ts.map +1 -0
  62. package/esm/services/default-dark-palette.js +56 -0
  63. package/esm/services/default-dark-palette.js.map +1 -0
  64. package/esm/services/default-dark-theme.d.ts +3 -0
  65. package/esm/services/default-dark-theme.d.ts.map +1 -1
  66. package/esm/services/default-dark-theme.js +7 -4
  67. package/esm/services/default-dark-theme.js.map +1 -1
  68. package/esm/services/default-light-theme.d.ts +3 -0
  69. package/esm/services/default-light-theme.d.ts.map +1 -1
  70. package/esm/services/default-light-theme.js +3 -0
  71. package/esm/services/default-light-theme.js.map +1 -1
  72. package/esm/services/index.d.ts +1 -0
  73. package/esm/services/index.d.ts.map +1 -1
  74. package/esm/services/index.js +1 -0
  75. package/esm/services/index.js.map +1 -1
  76. package/esm/services/theme-provider-service.d.ts +10 -1
  77. package/esm/services/theme-provider-service.d.ts.map +1 -1
  78. package/esm/services/theme-provider-service.js.map +1 -1
  79. package/package.json +2 -2
  80. package/src/components/form.spec.tsx +309 -0
  81. package/src/components/form.tsx +31 -8
  82. package/src/components/index.ts +1 -0
  83. package/src/components/markdown/index.ts +4 -0
  84. package/src/components/markdown/markdown-display.spec.tsx +243 -0
  85. package/src/components/markdown/markdown-display.tsx +202 -0
  86. package/src/components/markdown/markdown-editor.spec.tsx +142 -0
  87. package/src/components/markdown/markdown-editor.tsx +167 -0
  88. package/src/components/markdown/markdown-input.spec.tsx +274 -0
  89. package/src/components/markdown/markdown-input.tsx +143 -0
  90. package/src/components/markdown/markdown-parser.spec.ts +258 -0
  91. package/src/components/markdown/markdown-parser.ts +333 -0
  92. package/src/components/styles.tsx +1 -0
  93. package/src/components/typography.tsx +28 -15
  94. package/src/services/css-variable-theme.spec.ts +3 -0
  95. package/src/services/css-variable-theme.ts +3 -0
  96. package/src/services/default-dark-palette.ts +57 -0
  97. package/src/services/default-dark-theme.ts +7 -4
  98. package/src/services/default-light-theme.ts +3 -0
  99. package/src/services/index.ts +1 -0
  100. package/src/services/theme-provider-service.ts +7 -1
@@ -0,0 +1,202 @@
1
+ import { Shade, createComponent } from '@furystack/shades'
2
+ import { cssVariableTheme } from '../../services/css-variable-theme.js'
3
+ import { Checkbox } from '../inputs/checkbox.js'
4
+ import { Typography } from '../typography.js'
5
+ import type { InlineNode, MarkdownNode } from './markdown-parser.js'
6
+ import { parseMarkdown, toggleCheckbox } from './markdown-parser.js'
7
+
8
+ export type MarkdownDisplayProps = {
9
+ /** The raw Markdown string to render */
10
+ content: string
11
+ /** When false, checkboxes can be toggled. Defaults to true. */
12
+ readOnly?: boolean
13
+ /** Called with the updated Markdown string when a checkbox is toggled */
14
+ onChange?: (newContent: string) => void
15
+ }
16
+
17
+ const renderInline = (nodes: InlineNode[]): JSX.Element => {
18
+ return (
19
+ <>
20
+ {nodes.map((node) => {
21
+ switch (node.type) {
22
+ case 'text':
23
+ return <>{node.content}</>
24
+ case 'bold':
25
+ return <strong>{renderInline(node.children)}</strong>
26
+ case 'italic':
27
+ return <em>{renderInline(node.children)}</em>
28
+ case 'code':
29
+ return <code className="md-inline-code">{node.content}</code>
30
+ case 'link':
31
+ return (
32
+ <a className="md-link" href={node.href} target="_blank" rel="noopener noreferrer">
33
+ {renderInline(node.children)}
34
+ </a>
35
+ )
36
+ case 'image':
37
+ return <img className="md-image" src={node.src} alt={node.alt} />
38
+ default:
39
+ return <></>
40
+ }
41
+ })}
42
+ </>
43
+ )
44
+ }
45
+
46
+ const variantForLevel = (level: 1 | 2 | 3 | 4 | 5 | 6) => {
47
+ const map = { 1: 'h1', 2: 'h2', 3: 'h3', 4: 'h4', 5: 'h5', 6: 'h6' } as const
48
+ return map[level]
49
+ }
50
+
51
+ const renderBlock = (
52
+ node: MarkdownNode,
53
+ _index: number,
54
+ options: { content: string; readOnly: boolean; onChange?: (newContent: string) => void },
55
+ ): JSX.Element => {
56
+ switch (node.type) {
57
+ case 'heading':
58
+ return <Typography variant={variantForLevel(node.level)}>{renderInline(node.children)}</Typography>
59
+ case 'paragraph':
60
+ return <Typography variant="body1">{renderInline(node.children)}</Typography>
61
+ case 'codeBlock':
62
+ return (
63
+ <pre className="md-code-block" data-language={node.language || undefined}>
64
+ <code>{node.content}</code>
65
+ </pre>
66
+ )
67
+ case 'blockquote':
68
+ return (
69
+ <blockquote className="md-blockquote">
70
+ {node.children.map((child, i) => renderBlock(child, i, options))}
71
+ </blockquote>
72
+ )
73
+ case 'horizontalRule':
74
+ return <hr className="md-hr" />
75
+ case 'list': {
76
+ const listItems = node.items.map((item) => {
77
+ if (item.checkbox !== undefined) {
78
+ const handleChange = () => {
79
+ if (!options.readOnly && options.onChange) {
80
+ options.onChange(toggleCheckbox(options.content, item.sourceLineIndex))
81
+ }
82
+ }
83
+ return (
84
+ <li className="md-list-item md-checkbox-item" data-source-line={String(item.sourceLineIndex)}>
85
+ <Checkbox checked={item.checkbox === 'checked'} disabled={options.readOnly} onchange={handleChange} />
86
+ <span className="md-checkbox-label">{renderInline(item.children)}</span>
87
+ </li>
88
+ )
89
+ }
90
+ return <li className="md-list-item">{renderInline(item.children)}</li>
91
+ })
92
+ if (node.ordered) {
93
+ return <ol className="md-list">{listItems}</ol>
94
+ }
95
+ return <ul className="md-list">{listItems}</ul>
96
+ }
97
+ default:
98
+ return <></>
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Renders a Markdown string using FuryStack Shades components.
104
+ * Supports headings, paragraphs, lists, checkboxes, code blocks,
105
+ * blockquotes, images, links, and horizontal rules.
106
+ */
107
+ export const MarkdownDisplay = Shade<MarkdownDisplayProps>({
108
+ shadowDomName: 'shade-markdown-display',
109
+ css: {
110
+ display: 'block',
111
+ fontFamily: cssVariableTheme.typography.fontFamily,
112
+ color: cssVariableTheme.text.primary,
113
+ lineHeight: cssVariableTheme.typography.lineHeight.relaxed,
114
+
115
+ '& .md-inline-code': {
116
+ fontFamily: 'monospace',
117
+ backgroundColor: cssVariableTheme.action.hoverBackground,
118
+ padding: '2px 6px',
119
+ borderRadius: cssVariableTheme.shape.borderRadius.xs,
120
+ fontSize: '0.9em',
121
+ },
122
+
123
+ '& .md-code-block': {
124
+ fontFamily: 'monospace',
125
+ backgroundColor: cssVariableTheme.background.default,
126
+ border: `1px solid ${cssVariableTheme.action.subtleBorder}`,
127
+ borderRadius: cssVariableTheme.shape.borderRadius.md,
128
+ padding: cssVariableTheme.spacing.md,
129
+ overflow: 'auto',
130
+ fontSize: cssVariableTheme.typography.fontSize.sm,
131
+ margin: `${cssVariableTheme.spacing.sm} 0`,
132
+ },
133
+
134
+ '& .md-code-block code': {
135
+ font: 'inherit',
136
+ whiteSpace: 'pre',
137
+ },
138
+
139
+ '& .md-blockquote': {
140
+ borderLeft: `4px solid ${cssVariableTheme.palette.primary.main}`,
141
+ margin: `${cssVariableTheme.spacing.sm} 0`,
142
+ padding: `${cssVariableTheme.spacing.sm} ${cssVariableTheme.spacing.md}`,
143
+ color: cssVariableTheme.text.secondary,
144
+ },
145
+
146
+ '& .md-link': {
147
+ color: cssVariableTheme.palette.primary.main,
148
+ textDecoration: 'none',
149
+ },
150
+ '& .md-link:hover': {
151
+ textDecoration: 'underline',
152
+ },
153
+
154
+ '& .md-image': {
155
+ maxWidth: '100%',
156
+ borderRadius: cssVariableTheme.shape.borderRadius.md,
157
+ },
158
+
159
+ '& .md-hr': {
160
+ border: 'none',
161
+ borderTop: `1px solid ${cssVariableTheme.divider}`,
162
+ margin: `${cssVariableTheme.spacing.md} 0`,
163
+ },
164
+
165
+ '& .md-list': {
166
+ paddingLeft: cssVariableTheme.spacing.xl,
167
+ margin: `${cssVariableTheme.spacing.sm} 0`,
168
+ },
169
+
170
+ '& .md-list-item': {
171
+ marginBottom: cssVariableTheme.spacing.xs,
172
+ fontSize: cssVariableTheme.typography.fontSize.md,
173
+ },
174
+
175
+ '& .md-checkbox-item': {
176
+ listStyle: 'none',
177
+ display: 'flex',
178
+ alignItems: 'center',
179
+ gap: cssVariableTheme.spacing.sm,
180
+ },
181
+
182
+ '& .md-checkbox-label': {
183
+ fontSize: cssVariableTheme.typography.fontSize.md,
184
+ },
185
+ },
186
+ render: ({ props }) => {
187
+ const readOnly = props.readOnly !== false
188
+ const ast = parseMarkdown(props.content)
189
+
190
+ return (
191
+ <div className="md-root">
192
+ {ast.map((node, i) =>
193
+ renderBlock(node, i, {
194
+ content: props.content,
195
+ readOnly,
196
+ onChange: props.onChange,
197
+ }),
198
+ )}
199
+ </div>
200
+ )
201
+ },
202
+ })
@@ -0,0 +1,142 @@
1
+ import { Injector } from '@furystack/inject'
2
+ import { createComponent, initializeShadeRoot } from '@furystack/shades'
3
+ import { sleepAsync, usingAsync } from '@furystack/utils'
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
5
+ import { MarkdownEditor } from './markdown-editor.js'
6
+
7
+ describe('MarkdownEditor', () => {
8
+ beforeEach(() => {
9
+ document.body.innerHTML = '<div id="root"></div>'
10
+ })
11
+
12
+ afterEach(() => {
13
+ document.body.innerHTML = ''
14
+ vi.restoreAllMocks()
15
+ })
16
+
17
+ it('should render with shadow DOM', async () => {
18
+ await usingAsync(new Injector(), async (injector) => {
19
+ const rootElement = document.getElementById('root') as HTMLDivElement
20
+
21
+ initializeShadeRoot({
22
+ injector,
23
+ rootElement,
24
+ jsxElement: <MarkdownEditor value="# Hello" />,
25
+ })
26
+
27
+ await sleepAsync(50)
28
+
29
+ const el = document.querySelector('shade-markdown-editor')
30
+ expect(el).not.toBeNull()
31
+ })
32
+ })
33
+
34
+ describe('side-by-side layout (default)', () => {
35
+ it('should render input and preview panes side by side', async () => {
36
+ await usingAsync(new Injector(), async (injector) => {
37
+ const rootElement = document.getElementById('root') as HTMLDivElement
38
+
39
+ initializeShadeRoot({
40
+ injector,
41
+ rootElement,
42
+ jsxElement: <MarkdownEditor value="# Hello" />,
43
+ })
44
+
45
+ await sleepAsync(50)
46
+
47
+ const split = document.querySelector('.md-editor-split') as HTMLElement
48
+ expect(split).not.toBeNull()
49
+ expect(split.dataset.layout).toBe('side-by-side')
50
+
51
+ const input = document.querySelector('shade-markdown-editor shade-markdown-input')
52
+ expect(input).not.toBeNull()
53
+
54
+ const display = document.querySelector('shade-markdown-editor shade-markdown-display')
55
+ expect(display).not.toBeNull()
56
+ })
57
+ })
58
+ })
59
+
60
+ describe('above-below layout', () => {
61
+ it('should render with above-below layout', async () => {
62
+ await usingAsync(new Injector(), async (injector) => {
63
+ const rootElement = document.getElementById('root') as HTMLDivElement
64
+
65
+ initializeShadeRoot({
66
+ injector,
67
+ rootElement,
68
+ jsxElement: <MarkdownEditor value="# Hello" layout="above-below" />,
69
+ })
70
+
71
+ await sleepAsync(50)
72
+
73
+ const split = document.querySelector('.md-editor-split') as HTMLElement
74
+ expect(split).not.toBeNull()
75
+ expect(split.dataset.layout).toBe('above-below')
76
+ })
77
+ })
78
+ })
79
+
80
+ describe('tabs layout', () => {
81
+ it('should render with tabs layout', async () => {
82
+ await usingAsync(new Injector(), async (injector) => {
83
+ const rootElement = document.getElementById('root') as HTMLDivElement
84
+
85
+ initializeShadeRoot({
86
+ injector,
87
+ rootElement,
88
+ jsxElement: <MarkdownEditor value="# Hello" layout="tabs" />,
89
+ })
90
+
91
+ await sleepAsync(50)
92
+
93
+ const tabs = document.querySelector('shade-markdown-editor shade-tabs')
94
+ expect(tabs).not.toBeNull()
95
+
96
+ const tabButtons = document.querySelectorAll('shade-markdown-editor .shade-tab-btn')
97
+ expect(tabButtons.length).toBe(2)
98
+ })
99
+ })
100
+
101
+ it('should show the edit tab by default', async () => {
102
+ await usingAsync(new Injector(), async (injector) => {
103
+ const rootElement = document.getElementById('root') as HTMLDivElement
104
+
105
+ initializeShadeRoot({
106
+ injector,
107
+ rootElement,
108
+ jsxElement: <MarkdownEditor value="# Hello" layout="tabs" />,
109
+ })
110
+
111
+ await sleepAsync(50)
112
+
113
+ const input = document.querySelector('shade-markdown-editor shade-markdown-input')
114
+ expect(input).not.toBeNull()
115
+ })
116
+ })
117
+ })
118
+
119
+ it('should pass value to both input and display', async () => {
120
+ await usingAsync(new Injector(), async (injector) => {
121
+ const rootElement = document.getElementById('root') as HTMLDivElement
122
+ const mdContent = '# Test Content'
123
+
124
+ initializeShadeRoot({
125
+ injector,
126
+ rootElement,
127
+ jsxElement: <MarkdownEditor value={mdContent} />,
128
+ })
129
+
130
+ await sleepAsync(50)
131
+
132
+ const textarea = document.querySelector('shade-markdown-editor textarea') as HTMLTextAreaElement
133
+ expect(textarea.value).toBe(mdContent)
134
+
135
+ const heading = document.querySelector(
136
+ 'shade-markdown-editor shade-markdown-display shade-typography[data-variant="h1"]',
137
+ )
138
+ expect(heading).not.toBeNull()
139
+ expect(heading?.textContent).toContain('Test Content')
140
+ })
141
+ })
142
+ })
@@ -0,0 +1,167 @@
1
+ import { Shade, createComponent } from '@furystack/shades'
2
+ import { cssVariableTheme } from '../../services/css-variable-theme.js'
3
+ import { Tabs } from '../tabs.js'
4
+ import { MarkdownDisplay } from './markdown-display.js'
5
+ import { MarkdownInput } from './markdown-input.js'
6
+
7
+ export type MarkdownEditorLayout = 'side-by-side' | 'tabs' | 'above-below'
8
+
9
+ export type MarkdownEditorProps = {
10
+ /** The current Markdown string */
11
+ value: string
12
+ /** Called when the value changes (from either the input or checkbox toggle) */
13
+ onValueChange?: (newValue: string) => void
14
+ /** Layout mode for the editor. Defaults to 'side-by-side'. */
15
+ layout?: MarkdownEditorLayout
16
+ /** Maximum image file size in bytes for base64 paste */
17
+ maxImageSizeBytes?: number
18
+ /** When true, the editor is read-only */
19
+ readOnly?: boolean
20
+ /** Inline styles applied to the host element */
21
+ style?: Partial<CSSStyleDeclaration>
22
+ }
23
+
24
+ type TabType = 'edit' | 'preview'
25
+
26
+ /**
27
+ * Combined Markdown editor with an input pane and a live preview pane.
28
+ * Supports three layouts: side-by-side, tabs (Edit/Preview), or above-below.
29
+ */
30
+ export const MarkdownEditor = Shade<MarkdownEditorProps>({
31
+ shadowDomName: 'shade-markdown-editor',
32
+ css: {
33
+ display: 'flex',
34
+ flexDirection: 'column',
35
+ border: `1px solid ${cssVariableTheme.action.subtleBorder}`,
36
+ borderRadius: cssVariableTheme.shape.borderRadius.md,
37
+ overflow: 'hidden',
38
+ minHeight: '0',
39
+
40
+ '& .md-editor-split': {
41
+ display: 'flex',
42
+ flex: '1',
43
+ minHeight: '0',
44
+ },
45
+
46
+ '& .md-editor-split[data-layout="side-by-side"]': {
47
+ flexDirection: 'row',
48
+ },
49
+
50
+ '& .md-editor-split[data-layout="above-below"]': {
51
+ flexDirection: 'column',
52
+ minHeight: 'auto',
53
+ },
54
+
55
+ '& .md-editor-pane': {
56
+ flex: '1',
57
+ minWidth: '0',
58
+ minHeight: '0',
59
+ overflow: 'auto',
60
+ display: 'flex',
61
+ flexDirection: 'column',
62
+ },
63
+
64
+ '& .md-editor-pane-input': {
65
+ borderRight: 'none',
66
+ },
67
+
68
+ '& .md-editor-split[data-layout="side-by-side"] .md-editor-pane-input': {
69
+ borderRight: `1px solid ${cssVariableTheme.action.subtleBorder}`,
70
+ },
71
+
72
+ '& .md-editor-split[data-layout="above-below"] .md-editor-pane-input': {
73
+ borderBottom: `1px solid ${cssVariableTheme.action.subtleBorder}`,
74
+ },
75
+
76
+ '& .md-editor-split[data-layout="above-below"] .md-editor-pane': {
77
+ flex: 'none',
78
+ overflow: 'visible',
79
+ minHeight: 'auto',
80
+ },
81
+
82
+ '& .md-editor-split[data-layout="above-below"] shade-markdown-input textarea': {
83
+ overflow: 'hidden',
84
+ fieldSizing: 'content',
85
+ },
86
+
87
+ '& .md-editor-pane-preview': {
88
+ padding: cssVariableTheme.spacing.md,
89
+ },
90
+
91
+ '& shade-markdown-input': {
92
+ marginBottom: '0',
93
+ flex: '1',
94
+ display: 'flex',
95
+ flexDirection: 'column',
96
+ },
97
+ '& shade-markdown-input label': {
98
+ border: 'none',
99
+ borderRadius: '0',
100
+ flex: '1',
101
+ display: 'flex',
102
+ flexDirection: 'column',
103
+ },
104
+ '& shade-markdown-input textarea': {
105
+ flex: '1',
106
+ resize: 'none',
107
+ },
108
+
109
+ '& shade-tabs': {
110
+ flex: '1',
111
+ minHeight: '0',
112
+ },
113
+
114
+ '& .md-editor-tab-content': {
115
+ padding: cssVariableTheme.spacing.md,
116
+ overflow: 'auto',
117
+ },
118
+ },
119
+ render: ({ props, useState, useHostProps }) => {
120
+ const layout = props.layout ?? 'side-by-side'
121
+
122
+ useHostProps({
123
+ ...(props.style ? { style: props.style as Record<string, string> } : {}),
124
+ })
125
+
126
+ const [activeTab, setActiveTab] = useState<TabType>('activeTab', 'edit')
127
+
128
+ const inputPane = (
129
+ <MarkdownInput
130
+ value={props.value}
131
+ onValueChange={props.onValueChange}
132
+ maxImageSizeBytes={props.maxImageSizeBytes}
133
+ readOnly={props.readOnly}
134
+ />
135
+ )
136
+
137
+ const previewPane = <MarkdownDisplay content={props.value} readOnly={false} onChange={props.onValueChange} />
138
+
139
+ if (layout === 'tabs') {
140
+ return (
141
+ <Tabs
142
+ activeKey={activeTab}
143
+ onTabChange={(key) => setActiveTab(key as TabType)}
144
+ tabs={[
145
+ {
146
+ header: <>Edit</>,
147
+ hash: 'edit',
148
+ component: <div className="md-editor-tab-content">{inputPane}</div>,
149
+ },
150
+ {
151
+ header: <>Preview</>,
152
+ hash: 'preview',
153
+ component: <div className="md-editor-tab-content">{previewPane}</div>,
154
+ },
155
+ ]}
156
+ />
157
+ )
158
+ }
159
+
160
+ return (
161
+ <div className="md-editor-split" data-layout={layout}>
162
+ <div className="md-editor-pane md-editor-pane-input">{inputPane}</div>
163
+ <div className="md-editor-pane md-editor-pane-preview">{previewPane}</div>
164
+ </div>
165
+ )
166
+ },
167
+ })