@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.
- package/CHANGELOG.md +56 -0
- package/esm/components/form.d.ts +5 -2
- package/esm/components/form.d.ts.map +1 -1
- package/esm/components/form.js +28 -6
- package/esm/components/form.js.map +1 -1
- package/esm/components/form.spec.js +207 -0
- package/esm/components/form.spec.js.map +1 -1
- package/esm/components/index.d.ts +1 -0
- package/esm/components/index.d.ts.map +1 -1
- package/esm/components/index.js +1 -0
- package/esm/components/index.js.map +1 -1
- package/esm/components/markdown/index.d.ts +5 -0
- package/esm/components/markdown/index.d.ts.map +1 -0
- package/esm/components/markdown/index.js +5 -0
- package/esm/components/markdown/index.js.map +1 -0
- package/esm/components/markdown/markdown-display.d.ts +19 -0
- package/esm/components/markdown/markdown-display.d.ts.map +1 -0
- package/esm/components/markdown/markdown-display.js +149 -0
- package/esm/components/markdown/markdown-display.js.map +1 -0
- package/esm/components/markdown/markdown-display.spec.d.ts +2 -0
- package/esm/components/markdown/markdown-display.spec.d.ts.map +1 -0
- package/esm/components/markdown/markdown-display.spec.js +191 -0
- package/esm/components/markdown/markdown-display.spec.js.map +1 -0
- package/esm/components/markdown/markdown-editor.d.ts +25 -0
- package/esm/components/markdown/markdown-editor.d.ts.map +1 -0
- package/esm/components/markdown/markdown-editor.js +113 -0
- package/esm/components/markdown/markdown-editor.js.map +1 -0
- package/esm/components/markdown/markdown-editor.spec.d.ts +2 -0
- package/esm/components/markdown/markdown-editor.spec.d.ts.map +1 -0
- package/esm/components/markdown/markdown-editor.spec.js +111 -0
- package/esm/components/markdown/markdown-editor.spec.js.map +1 -0
- package/esm/components/markdown/markdown-input.d.ts +29 -0
- package/esm/components/markdown/markdown-input.d.ts.map +1 -0
- package/esm/components/markdown/markdown-input.js +100 -0
- package/esm/components/markdown/markdown-input.js.map +1 -0
- package/esm/components/markdown/markdown-input.spec.d.ts +2 -0
- package/esm/components/markdown/markdown-input.spec.d.ts.map +1 -0
- package/esm/components/markdown/markdown-input.spec.js +215 -0
- package/esm/components/markdown/markdown-input.spec.js.map +1 -0
- package/esm/components/markdown/markdown-parser.d.ts +82 -0
- package/esm/components/markdown/markdown-parser.d.ts.map +1 -0
- package/esm/components/markdown/markdown-parser.js +274 -0
- package/esm/components/markdown/markdown-parser.js.map +1 -0
- package/esm/components/markdown/markdown-parser.spec.d.ts +2 -0
- package/esm/components/markdown/markdown-parser.spec.d.ts.map +1 -0
- package/esm/components/markdown/markdown-parser.spec.js +229 -0
- package/esm/components/markdown/markdown-parser.spec.js.map +1 -0
- package/esm/components/styles.d.ts +1 -0
- package/esm/components/styles.d.ts.map +1 -1
- package/esm/components/styles.js.map +1 -1
- package/esm/components/typography.d.ts.map +1 -1
- package/esm/components/typography.js +26 -14
- package/esm/components/typography.js.map +1 -1
- package/esm/services/css-variable-theme.d.ts +3 -0
- package/esm/services/css-variable-theme.d.ts.map +1 -1
- package/esm/services/css-variable-theme.js +3 -0
- package/esm/services/css-variable-theme.js.map +1 -1
- package/esm/services/css-variable-theme.spec.js +3 -0
- package/esm/services/css-variable-theme.spec.js.map +1 -1
- package/esm/services/default-dark-palette.d.ts +8 -0
- package/esm/services/default-dark-palette.d.ts.map +1 -0
- package/esm/services/default-dark-palette.js +56 -0
- package/esm/services/default-dark-palette.js.map +1 -0
- package/esm/services/default-dark-theme.d.ts +3 -0
- package/esm/services/default-dark-theme.d.ts.map +1 -1
- package/esm/services/default-dark-theme.js +7 -4
- package/esm/services/default-dark-theme.js.map +1 -1
- package/esm/services/default-light-theme.d.ts +3 -0
- package/esm/services/default-light-theme.d.ts.map +1 -1
- package/esm/services/default-light-theme.js +3 -0
- package/esm/services/default-light-theme.js.map +1 -1
- package/esm/services/index.d.ts +1 -0
- package/esm/services/index.d.ts.map +1 -1
- package/esm/services/index.js +1 -0
- package/esm/services/index.js.map +1 -1
- package/esm/services/theme-provider-service.d.ts +10 -1
- package/esm/services/theme-provider-service.d.ts.map +1 -1
- package/esm/services/theme-provider-service.js.map +1 -1
- package/package.json +2 -2
- package/src/components/form.spec.tsx +309 -0
- package/src/components/form.tsx +31 -8
- package/src/components/index.ts +1 -0
- package/src/components/markdown/index.ts +4 -0
- package/src/components/markdown/markdown-display.spec.tsx +243 -0
- package/src/components/markdown/markdown-display.tsx +202 -0
- package/src/components/markdown/markdown-editor.spec.tsx +142 -0
- package/src/components/markdown/markdown-editor.tsx +167 -0
- package/src/components/markdown/markdown-input.spec.tsx +274 -0
- package/src/components/markdown/markdown-input.tsx +143 -0
- package/src/components/markdown/markdown-parser.spec.ts +258 -0
- package/src/components/markdown/markdown-parser.ts +333 -0
- package/src/components/styles.tsx +1 -0
- package/src/components/typography.tsx +28 -15
- package/src/services/css-variable-theme.spec.ts +3 -0
- package/src/services/css-variable-theme.ts +3 -0
- package/src/services/default-dark-palette.ts +57 -0
- package/src/services/default-dark-theme.ts +7 -4
- package/src/services/default-light-theme.ts +3 -0
- package/src/services/index.ts +1 -0
- 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
|
+
})
|