@synerise/ds-rich-text 0.1.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 +10 -0
- package/LICENSE.md +21 -0
- package/README.md +173 -0
- package/dist/RichText.d.ts +4 -0
- package/dist/RichText.js +179 -0
- package/dist/RichText.styles.d.ts +31 -0
- package/dist/RichText.styles.js +87 -0
- package/dist/RichText.types.d.ts +139 -0
- package/dist/RichText.types.js +44 -0
- package/dist/blockModel/blockModel.types.d.ts +130 -0
- package/dist/blockModel/blockModel.types.js +1 -0
- package/dist/blockModel/index.d.ts +2 -0
- package/dist/blockModel/index.js +14 -0
- package/dist/blockModel/serializers/documentToHtml.d.ts +2 -0
- package/dist/blockModel/serializers/documentToHtml.js +101 -0
- package/dist/blockModel/serializers/documentToMarkdown.d.ts +2 -0
- package/dist/blockModel/serializers/documentToMarkdown.js +84 -0
- package/dist/blockModel/serializers/documentToTiptap.d.ts +2 -0
- package/dist/blockModel/serializers/documentToTiptap.js +133 -0
- package/dist/blockModel/serializers/htmlToDocument.d.ts +2 -0
- package/dist/blockModel/serializers/htmlToDocument.js +242 -0
- package/dist/blockModel/serializers/index.d.ts +6 -0
- package/dist/blockModel/serializers/index.js +14 -0
- package/dist/blockModel/serializers/markdownToDocument.d.ts +10 -0
- package/dist/blockModel/serializers/markdownToDocument.js +11 -0
- package/dist/blockModel/serializers/tiptapToDocument.d.ts +2 -0
- package/dist/blockModel/serializers/tiptapToDocument.js +189 -0
- package/dist/components/ImagePopover.d.ts +3 -0
- package/dist/components/ImagePopover.js +89 -0
- package/dist/components/LinkPopover.d.ts +3 -0
- package/dist/components/LinkPopover.js +57 -0
- package/dist/components/Toolbar.d.ts +3 -0
- package/dist/components/Toolbar.js +436 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +8 -0
- package/dist/extensions/CodeSnippet.d.ts +19 -0
- package/dist/extensions/CodeSnippet.js +76 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +52 -0
- package/dist/modules.d.js +1 -0
- package/dist/modules.d.ts +4 -0
- package/dist/utils/computeVisibleCount.d.ts +6 -0
- package/dist/utils/computeVisibleCount.js +19 -0
- package/dist/utils/getExtensions.d.ts +13 -0
- package/dist/utils/getExtensions.js +80 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/normalizeUrl.d.ts +7 -0
- package/dist/utils/normalizeUrl.js +13 -0
- package/dist/utils/sanitizeUrl.d.ts +9 -0
- package/dist/utils/sanitizeUrl.js +30 -0
- package/package.json +75 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
+
|
|
6
|
+
# 0.1.0 (2026-07-24)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- **rich-text:** add markdown serializers, code snippet and table support ([56acd09](https://github.com/Synerise/synerise-design/commit/56acd097e2106a8743e85f98226caedca87a33e3))
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 Synerise
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: rich-text
|
|
3
|
+
title: RichText
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
RichText UI Component — a WYSIWYG editor for the Synerise Design System, built on TipTap (ProseMirror).
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
npm i @synerise/ds-rich-text
|
|
12
|
+
or
|
|
13
|
+
yarn add @synerise/ds-rich-text
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Usage
|
|
17
|
+
|
|
18
|
+
### HTML mode (default)
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import RichText from '@synerise/ds-rich-text';
|
|
22
|
+
|
|
23
|
+
<RichText
|
|
24
|
+
label="Description"
|
|
25
|
+
placeholder="Start typing..."
|
|
26
|
+
value={html}
|
|
27
|
+
onChange={(html) => setHtml(html)}
|
|
28
|
+
/>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Document mode (Portable Block Model)
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import RichText from '@synerise/ds-rich-text';
|
|
35
|
+
import type { RichTextDocument } from '@synerise/ds-rich-text';
|
|
36
|
+
|
|
37
|
+
const [doc, setDoc] = useState<RichTextDocument>();
|
|
38
|
+
|
|
39
|
+
<RichText
|
|
40
|
+
format="document"
|
|
41
|
+
label="Article body"
|
|
42
|
+
value={doc}
|
|
43
|
+
onChange={(doc) => setDoc(doc)}
|
|
44
|
+
/>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### With image upload
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
<RichText
|
|
51
|
+
value={content}
|
|
52
|
+
onChange={setContent}
|
|
53
|
+
onImageUpload={async (file) => {
|
|
54
|
+
const formData = new FormData();
|
|
55
|
+
formData.append('file', file);
|
|
56
|
+
const res = await fetch('/api/media/upload', { method: 'POST', body: formData });
|
|
57
|
+
const { url } = await res.json();
|
|
58
|
+
return url;
|
|
59
|
+
}}
|
|
60
|
+
/>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Limited toolbar
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
<RichText
|
|
67
|
+
toolbarFeatures={['bold', 'italic', 'link']}
|
|
68
|
+
value={content}
|
|
69
|
+
onChange={setContent}
|
|
70
|
+
/>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## API
|
|
74
|
+
|
|
75
|
+
| Property | Description | Type | Default |
|
|
76
|
+
| --- | --- | --- | --- |
|
|
77
|
+
| format | Output format — `'html'` emits string, `'document'` emits `RichTextDocument` | `'html' \| 'document'` | `'html'` |
|
|
78
|
+
| value | Controlled value (accepts both HTML string and `RichTextDocument`) | `string \| RichTextDocument` | - |
|
|
79
|
+
| defaultValue | Initial value for uncontrolled mode | `string \| RichTextDocument` | - |
|
|
80
|
+
| onChange | Called when content changes | `(value: string \| RichTextDocument) => void` | - |
|
|
81
|
+
| onEditorReady | Called with the TipTap Editor instance after mount | `(editor: Editor) => void` | - |
|
|
82
|
+
| placeholder | Placeholder text when editor is empty | `string` | - |
|
|
83
|
+
| readOnly | Makes the editor non-editable | `boolean` | `false` |
|
|
84
|
+
| disabled | Disables the editor (greyed out) | `boolean` | `false` |
|
|
85
|
+
| label | Label above the editor | `ReactNode` | - |
|
|
86
|
+
| tooltip | Tooltip content next to label | `ReactNode` | - |
|
|
87
|
+
| tooltipConfig | Full tooltip configuration | `TooltipProps` | - |
|
|
88
|
+
| description | Description text below editor | `ReactNode` | - |
|
|
89
|
+
| errorText | Error message below editor (triggers error styling) | `ReactNode` | - |
|
|
90
|
+
| height | Height of the editor area | `string \| number` | - |
|
|
91
|
+
| maxHeight | Max height before scrolling | `string \| number` | - |
|
|
92
|
+
| toolbarFeatures | Which toolbar buttons to show | `ToolbarFeature[]` | all features |
|
|
93
|
+
| hideToolbar | Hide the toolbar entirely | `boolean` | `false` |
|
|
94
|
+
| headingLevels | Available heading levels | `(1 \| 2 \| 3 \| 4 \| 5 \| 6)[]` | `[1, 2, 3]` |
|
|
95
|
+
| texts | Override i18n strings | `Partial<RichTextTexts>` | - |
|
|
96
|
+
| noBorder | Remove the border | `boolean` | `false` |
|
|
97
|
+
| autoFocus | Auto-focus the editor on mount | `boolean` | `false` |
|
|
98
|
+
| onImageUpload | Custom image upload handler; receives File, returns URL | `(file: File) => string \| Promise<string>` | - |
|
|
99
|
+
| style | Inline styles on wrapper | `CSSProperties` | - |
|
|
100
|
+
| className | Class name on wrapper | `string` | - |
|
|
101
|
+
|
|
102
|
+
### ToolbarFeature
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
'bold' | 'italic' | 'underline' | 'strikethrough' | 'heading' |
|
|
106
|
+
'bulletList' | 'orderedList' | 'link' | 'textAlign' | 'codeBlock' | 'image'
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### RichTextTexts
|
|
110
|
+
|
|
111
|
+
Customisable user-facing labels:
|
|
112
|
+
|
|
113
|
+
| Property | Description | Default |
|
|
114
|
+
| --- | --- | --- |
|
|
115
|
+
| bold | Bold button tooltip | `'Bold'` |
|
|
116
|
+
| italic | Italic button tooltip | `'Italic'` |
|
|
117
|
+
| underline | Underline button tooltip | `'Underline'` |
|
|
118
|
+
| strikethrough | Strikethrough button tooltip | `'Strikethrough'` |
|
|
119
|
+
| heading | Heading button tooltip prefix | `'Heading'` |
|
|
120
|
+
| bulletList | Bullet list button tooltip | `'Bullet list'` |
|
|
121
|
+
| orderedList | Ordered list button tooltip | `'Ordered list'` |
|
|
122
|
+
| link | Link button tooltip | `'Link'` |
|
|
123
|
+
| alignLeft | Align left button tooltip | `'Align left'` |
|
|
124
|
+
| alignCenter | Align center button tooltip | `'Align center'` |
|
|
125
|
+
| alignRight | Align right button tooltip | `'Align right'` |
|
|
126
|
+
| codeBlock | Code block button tooltip | `'Code block'` |
|
|
127
|
+
| image | Image button tooltip | `'Image'` |
|
|
128
|
+
| linkPlaceholder | Link URL input placeholder | `'Enter URL...'` |
|
|
129
|
+
| linkConfirm | Link confirm button label | `'Apply'` |
|
|
130
|
+
| linkRemove | Link remove button label | `'Remove link'` |
|
|
131
|
+
| imagePlaceholder | Image URL input placeholder | `'Enter image URL...'` |
|
|
132
|
+
| imageConfirm | Image confirm button label | `'Insert'` |
|
|
133
|
+
| imageUpload | Image upload button label | `'Upload file'` |
|
|
134
|
+
|
|
135
|
+
## Block Model
|
|
136
|
+
|
|
137
|
+
The component can output a structured `RichTextDocument` instead of HTML when `format="document"` is set. This is an editor-agnostic JSON format suitable for database storage and independent rendering.
|
|
138
|
+
|
|
139
|
+
### Serializers
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
import {
|
|
143
|
+
htmlToDocument, // HTML string → RichTextDocument
|
|
144
|
+
documentToHtml, // RichTextDocument → HTML string
|
|
145
|
+
tiptapJsonToDocument, // ProseMirror JSON → RichTextDocument
|
|
146
|
+
documentToTiptapJson, // RichTextDocument → ProseMirror JSON
|
|
147
|
+
} from '@synerise/ds-rich-text';
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### RichTextDocument structure
|
|
151
|
+
|
|
152
|
+
```json
|
|
153
|
+
{
|
|
154
|
+
"type": "doc",
|
|
155
|
+
"version": 1,
|
|
156
|
+
"children": [
|
|
157
|
+
{
|
|
158
|
+
"type": "heading",
|
|
159
|
+
"attrs": { "level": 2 },
|
|
160
|
+
"children": [{ "type": "text", "text": "Title" }]
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"type": "paragraph",
|
|
164
|
+
"children": [
|
|
165
|
+
{ "type": "text", "text": "Text with " },
|
|
166
|
+
{ "type": "text", "text": "bold", "marks": [{ "type": "bold" }] }
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
]
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
|
package/dist/RichText.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef, useState, useRef, useMemo, useCallback, useEffect } from "react";
|
|
3
|
+
import { useTheme } from "@synerise/ds-core";
|
|
4
|
+
import { FormFieldLabel } from "@synerise/ds-form-field";
|
|
5
|
+
import Icon, { EditS } from "@synerise/ds-icon";
|
|
6
|
+
import { useCombinedRefs, useOnClickOutside } from "@synerise/ds-utils";
|
|
7
|
+
import { useEditor, EditorContent } from "@tiptap/react";
|
|
8
|
+
import { RichTextWrapper, ContentAbove, EditorWrapper, ToolbarArea, EditorArea, SubtleSuffix, ContentBelow, ErrorText, Description } from "./RichText.styles.js";
|
|
9
|
+
import { DEFAULT_TEXTS, ALL_TOOLBAR_FEATURES, DEFAULT_HEADING_LEVELS } from "./RichText.types.js";
|
|
10
|
+
import { tiptapJsonToDocument } from "./blockModel/serializers/tiptapToDocument.js";
|
|
11
|
+
import { documentToTiptapJson } from "./blockModel/serializers/documentToTiptap.js";
|
|
12
|
+
import { documentToMarkdown } from "./blockModel/serializers/documentToMarkdown.js";
|
|
13
|
+
import { markdownToDocument } from "./blockModel/serializers/markdownToDocument.js";
|
|
14
|
+
import { Toolbar } from "./components/Toolbar.js";
|
|
15
|
+
import "@synerise/ds-button";
|
|
16
|
+
import "@synerise/ds-input";
|
|
17
|
+
import { getExtensions } from "./utils/getExtensions.js";
|
|
18
|
+
const SUBTLE_IGNORE_SELECTORS = ["[data-popover-content]"];
|
|
19
|
+
const isDocument = (val) => typeof val === "object" && val !== null && val.type === "doc";
|
|
20
|
+
const toEditorContent = (val, format) => {
|
|
21
|
+
if (val === void 0) {
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
if (typeof val === "string") {
|
|
25
|
+
return format === "markdown" ? documentToTiptapJson(markdownToDocument(val)) : val;
|
|
26
|
+
}
|
|
27
|
+
return documentToTiptapJson(val);
|
|
28
|
+
};
|
|
29
|
+
const RichText = forwardRef(({
|
|
30
|
+
format = "html",
|
|
31
|
+
value,
|
|
32
|
+
defaultValue,
|
|
33
|
+
onChange,
|
|
34
|
+
onEditorReady,
|
|
35
|
+
placeholder,
|
|
36
|
+
readOnly = false,
|
|
37
|
+
disabled = false,
|
|
38
|
+
label,
|
|
39
|
+
tooltip,
|
|
40
|
+
tooltipConfig,
|
|
41
|
+
description,
|
|
42
|
+
errorText,
|
|
43
|
+
height,
|
|
44
|
+
maxHeight,
|
|
45
|
+
toolbarFeatures = ALL_TOOLBAR_FEATURES,
|
|
46
|
+
hideToolbar = false,
|
|
47
|
+
subtle = false,
|
|
48
|
+
animations = false,
|
|
49
|
+
headingLevels = DEFAULT_HEADING_LEVELS,
|
|
50
|
+
texts: textsProp,
|
|
51
|
+
noBorder,
|
|
52
|
+
style,
|
|
53
|
+
className,
|
|
54
|
+
autoFocus = false,
|
|
55
|
+
onImageUpload,
|
|
56
|
+
onEditWithAI,
|
|
57
|
+
editWithAIOptions
|
|
58
|
+
}, ref) => {
|
|
59
|
+
const [isFocused, setIsFocused] = useState(false);
|
|
60
|
+
const [subtleActive, setSubtleActive] = useState(false);
|
|
61
|
+
const [subtleBlurred, setSubtleBlurred] = useState(false);
|
|
62
|
+
const lastEmittedRef = useRef("");
|
|
63
|
+
const wrapperRef = useCombinedRefs(ref);
|
|
64
|
+
const theme = useTheme();
|
|
65
|
+
const hasError = !!errorText;
|
|
66
|
+
const isSubtle = subtle && !readOnly && !disabled;
|
|
67
|
+
const isSubtlePreview = isSubtle && !subtleActive && !hasError;
|
|
68
|
+
const isEditable = !readOnly && !disabled && !isSubtlePreview;
|
|
69
|
+
const texts = useMemo(() => ({
|
|
70
|
+
...DEFAULT_TEXTS,
|
|
71
|
+
...textsProp
|
|
72
|
+
}), [textsProp]);
|
|
73
|
+
const extensions = useMemo(() => getExtensions({
|
|
74
|
+
features: toolbarFeatures,
|
|
75
|
+
headingLevels,
|
|
76
|
+
placeholder,
|
|
77
|
+
copyText: texts.copySnippet,
|
|
78
|
+
copiedText: texts.copiedSnippet,
|
|
79
|
+
// Like SubtleForm fields, an empty subtle preview shows the placeholder.
|
|
80
|
+
alwaysShowPlaceholder: isSubtle
|
|
81
|
+
}), [toolbarFeatures, headingLevels, placeholder, texts, isSubtle]);
|
|
82
|
+
const handleUpdate = useCallback(({
|
|
83
|
+
editor: ed
|
|
84
|
+
}) => {
|
|
85
|
+
if (!onChange) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (format === "document") {
|
|
89
|
+
lastEmittedRef.current = ed.getHTML();
|
|
90
|
+
onChange(tiptapJsonToDocument(ed.getJSON()));
|
|
91
|
+
} else if (format === "markdown") {
|
|
92
|
+
const md = documentToMarkdown(tiptapJsonToDocument(ed.getJSON()));
|
|
93
|
+
lastEmittedRef.current = md;
|
|
94
|
+
onChange(md);
|
|
95
|
+
} else {
|
|
96
|
+
const html = ed.getHTML();
|
|
97
|
+
lastEmittedRef.current = html;
|
|
98
|
+
onChange(html);
|
|
99
|
+
}
|
|
100
|
+
}, [format, onChange]);
|
|
101
|
+
const editor = useEditor({
|
|
102
|
+
extensions,
|
|
103
|
+
content: toEditorContent(value ?? defaultValue, format),
|
|
104
|
+
editable: isEditable,
|
|
105
|
+
autofocus: autoFocus,
|
|
106
|
+
onUpdate: handleUpdate,
|
|
107
|
+
onFocus: () => setIsFocused(true),
|
|
108
|
+
onBlur: () => setIsFocused(false)
|
|
109
|
+
});
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (editor && onEditorReady) {
|
|
112
|
+
onEditorReady(editor);
|
|
113
|
+
}
|
|
114
|
+
}, [editor, onEditorReady]);
|
|
115
|
+
useEffect(() => {
|
|
116
|
+
if (editor) {
|
|
117
|
+
editor.setEditable(isEditable);
|
|
118
|
+
}
|
|
119
|
+
}, [editor, isEditable]);
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (isSubtle && subtleActive && editor && !editor.isDestroyed) {
|
|
122
|
+
editor.commands.focus("end");
|
|
123
|
+
}
|
|
124
|
+
}, [isSubtle, subtleActive, editor]);
|
|
125
|
+
const handleSubtleActivate = useCallback((event) => {
|
|
126
|
+
if (event.target.closest("a")) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
setSubtleActive(true);
|
|
130
|
+
setSubtleBlurred(false);
|
|
131
|
+
}, []);
|
|
132
|
+
const handleSubtleDeactivate = useCallback(() => {
|
|
133
|
+
setSubtleActive(false);
|
|
134
|
+
setSubtleBlurred(true);
|
|
135
|
+
}, []);
|
|
136
|
+
useOnClickOutside(wrapperRef, isSubtle && subtleActive ? handleSubtleDeactivate : null, void 0, SUBTLE_IGNORE_SELECTORS);
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (!editor || value === void 0) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (isDocument(value)) {
|
|
142
|
+
const pmJson = documentToTiptapJson(value);
|
|
143
|
+
const currentJson = JSON.stringify(editor.getJSON());
|
|
144
|
+
const newJson = JSON.stringify(pmJson);
|
|
145
|
+
if (currentJson !== newJson) {
|
|
146
|
+
editor.commands.setContent(pmJson, false);
|
|
147
|
+
}
|
|
148
|
+
} else if (format === "markdown") {
|
|
149
|
+
if (lastEmittedRef.current !== value) {
|
|
150
|
+
editor.commands.setContent(documentToTiptapJson(markdownToDocument(value)), false);
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
const currentHTML = editor.getHTML();
|
|
154
|
+
if (currentHTML !== value && lastEmittedRef.current !== value) {
|
|
155
|
+
editor.commands.setContent(value, false);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}, [editor, value, format]);
|
|
159
|
+
const labelElement = useMemo(() => label && /* @__PURE__ */ jsx(FormFieldLabel, { label, tooltip, tooltipConfig: tooltip || tooltipConfig ? {
|
|
160
|
+
...tooltipConfig,
|
|
161
|
+
title: tooltip
|
|
162
|
+
} : void 0 }), [label, tooltip, tooltipConfig]);
|
|
163
|
+
return /* @__PURE__ */ jsxs(RichTextWrapper, { ref: wrapperRef, $disabled: disabled, style, className, "data-testid": "rich-text-wrapper", children: [
|
|
164
|
+
labelElement && /* @__PURE__ */ jsx(ContentAbove, { "data-testid": "rich-text-label", children: labelElement }),
|
|
165
|
+
/* @__PURE__ */ jsxs(EditorWrapper, { $hasError: hasError, $noBorder: noBorder, $isFocused: isFocused, $subtlePreview: isSubtlePreview, $subtleBlurred: subtleBlurred, $animations: animations, onClick: isSubtlePreview ? handleSubtleActivate : void 0, "data-testid": "rich-text-editor-wrapper", children: [
|
|
166
|
+
!hideToolbar && !readOnly && !isSubtlePreview && /* @__PURE__ */ jsx(ToolbarArea, { children: /* @__PURE__ */ jsx(Toolbar, { editor, features: toolbarFeatures, headingLevels, texts, disabled, onImageUpload, onEditWithAI, editWithAIOptions }) }),
|
|
167
|
+
/* @__PURE__ */ jsx(EditorArea, { $height: height, $maxHeight: maxHeight, $readOnly: readOnly, $subtlePreview: isSubtlePreview, "data-testid": "rich-text-editor", children: /* @__PURE__ */ jsx(EditorContent, { editor }) }),
|
|
168
|
+
isSubtlePreview && /* @__PURE__ */ jsx(SubtleSuffix, { "data-testid": "rich-text-subtle-edit-icon", children: /* @__PURE__ */ jsx(Icon, { component: /* @__PURE__ */ jsx(EditS, {}), color: theme.palette["grey-600"] }) })
|
|
169
|
+
] }),
|
|
170
|
+
(errorText || description) && /* @__PURE__ */ jsxs(ContentBelow, { "data-testid": "rich-text-below", children: [
|
|
171
|
+
errorText && /* @__PURE__ */ jsx(ErrorText, { "data-testid": "rich-text-error", children: errorText }),
|
|
172
|
+
description && /* @__PURE__ */ jsx(Description, { "data-testid": "rich-text-description", children: description })
|
|
173
|
+
] })
|
|
174
|
+
] });
|
|
175
|
+
});
|
|
176
|
+
RichText.displayName = "RichText";
|
|
177
|
+
export {
|
|
178
|
+
RichText as default
|
|
179
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ThemeProps } from '@synerise/ds-core';
|
|
2
|
+
export declare const RichTextWrapper: import('styled-components').StyledComponent<"div", any, {
|
|
3
|
+
$disabled?: boolean;
|
|
4
|
+
}, never>;
|
|
5
|
+
export declare const SubtleSuffix: import('styled-components').StyledComponent<"div", any, {}, never>;
|
|
6
|
+
export declare const EditorWrapper: import('styled-components').StyledComponent<"div", any, {
|
|
7
|
+
$hasError?: boolean;
|
|
8
|
+
$noBorder?: boolean;
|
|
9
|
+
$isFocused?: boolean;
|
|
10
|
+
$subtlePreview?: boolean;
|
|
11
|
+
$subtleBlurred?: boolean;
|
|
12
|
+
$animations?: boolean;
|
|
13
|
+
}, never>;
|
|
14
|
+
export declare const ToolbarArea: import('styled-components').StyledComponent<"div", any, ThemeProps, never>;
|
|
15
|
+
export declare const ToolbarItems: import('styled-components').StyledComponent<"div", any, {}, never>;
|
|
16
|
+
export declare const ToolbarEntry: import('styled-components').StyledComponent<"span", any, {}, never>;
|
|
17
|
+
export declare const EditorArea: import('styled-components').StyledComponent<"div", any, {
|
|
18
|
+
$height?: string | number;
|
|
19
|
+
$maxHeight?: string | number;
|
|
20
|
+
$readOnly?: boolean;
|
|
21
|
+
$subtlePreview?: boolean;
|
|
22
|
+
}, never>;
|
|
23
|
+
export declare const ContentAbove: import('styled-components').StyledComponent<"div", any, {}, never>;
|
|
24
|
+
export declare const ContentBelow: import('styled-components').StyledComponent<"div", any, {}, never>;
|
|
25
|
+
export declare const ErrorText: import('styled-components').StyledComponent<"div", any, ThemeProps, never>;
|
|
26
|
+
export declare const Description: import('styled-components').StyledComponent<"div", any, ThemeProps, never>;
|
|
27
|
+
export declare const AiGradientLabel: import('styled-components').StyledComponent<"span", any, {}, never>;
|
|
28
|
+
export declare const ToolbarSeparator: import('styled-components').StyledComponent<"div", any, ThemeProps, never>;
|
|
29
|
+
export declare const BlockMenu: import('styled-components').StyledComponent<"div", any, {}, never>;
|
|
30
|
+
export declare const PopoverForm: import('styled-components').StyledComponent<"div", any, {}, never>;
|
|
31
|
+
export declare const PopoverActions: import('styled-components').StyledComponent<"div", any, {}, never>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import styled, { css, keyframes } from "styled-components";
|
|
2
|
+
import { hexToRgba } from "@synerise/ds-utils";
|
|
3
|
+
const TOOLBAR_HEIGHT_PX = 44;
|
|
4
|
+
const subtleBlurAnimation = /* @__PURE__ */ keyframes(["0%{padding-top:", "px;}100%{padding-top:12px;}"], 12 + TOOLBAR_HEIGHT_PX);
|
|
5
|
+
const RichTextWrapper = /* @__PURE__ */ styled.div.withConfig({
|
|
6
|
+
displayName: "RichTextstyles__RichTextWrapper",
|
|
7
|
+
componentId: "sc-8lgglv-0"
|
|
8
|
+
})(["display:flex;flex-direction:column;", ";"], (props) => props.$disabled && css(["opacity:0.4;pointer-events:none;"]));
|
|
9
|
+
const SubtleSuffix = /* @__PURE__ */ styled.div.withConfig({
|
|
10
|
+
displayName: "RichTextstyles__SubtleSuffix",
|
|
11
|
+
componentId: "sc-8lgglv-1"
|
|
12
|
+
})(["position:absolute;right:6px;top:6px;display:flex;opacity:0;height:24px;transition:opacity 0.1s ease-in;transition-delay:0.2s;pointer-events:none;"]);
|
|
13
|
+
const EditorWrapper = /* @__PURE__ */ styled.div.withConfig({
|
|
14
|
+
displayName: "RichTextstyles__EditorWrapper",
|
|
15
|
+
componentId: "sc-8lgglv-2"
|
|
16
|
+
})(["position:relative;border:", ";border-radius:3px;display:flex;flex-direction:column;overflow:hidden;transition:border-color 0.2s ease,background 0.1s ease-in;", ";", ";", ";", ";"], (props) => props.$noBorder ? "none" : props.$subtlePreview ? "solid 1px transparent" : `solid 1px ${props.$hasError ? props.theme.palette["red-600"] : props.$isFocused ? props.theme.palette["blue-600"] : props.theme.palette["grey-200"]}`, (props) => props.$subtlePreview && css(["cursor:pointer;&& .ProseMirror{cursor:pointer;padding-left:0;}&&:hover{background:", ";", "{opacity:1;}}", ";"], hexToRgba(props.theme.palette["grey-300"], 0.4), SubtleSuffix, props.$animations ? css(["transition:border-color 0.2s ease,background 0.1s ease-in 0.2s;&& .ProseMirror{transition:padding 0.1s ease-in 0.2s;}&& .ProseMirror p.is-editor-empty:first-child::before{transform:translateY(-6px);transition:transform 0.1s ease-in 0.2s;}&&:hover{.ProseMirror{padding-left:12px;}.ProseMirror p.is-editor-empty:first-child::before{transform:translateY(0);}}"]) : css(["transition:border-color 0.2s ease;", "{transition:none;}"], SubtleSuffix)), (props) => props.$subtlePreview && props.$subtleBlurred && props.$animations && css(["&& .ProseMirror{animation:", " 0.1s ease-in 0.2s backwards;}"], subtleBlurAnimation), (props) => props.$hasError && css(["background:", ";", ";"], props.theme.palette["red-050"], !props.$noBorder && css(["outline:1px solid ", ";outline-offset:-1px;"], props.theme.palette["red-600"])), (props) => props.$isFocused && !props.$hasError && !props.$noBorder && css(["outline:1px solid ", ";outline-offset:-1px;"], props.theme.palette["blue-600"]));
|
|
17
|
+
const ToolbarArea = /* @__PURE__ */ styled.div.withConfig({
|
|
18
|
+
displayName: "RichTextstyles__ToolbarArea",
|
|
19
|
+
componentId: "sc-8lgglv-3"
|
|
20
|
+
})(["padding:4px;box-shadow:inset 0 -1px 0 0 ", ";> div{> div{flex:0 0 auto;background:transparent;box-shadow:none;padding:2px;border-radius:0;}}"], (props) => props.theme.palette["grey-200"]);
|
|
21
|
+
const ToolbarItems = /* @__PURE__ */ styled.div.withConfig({
|
|
22
|
+
displayName: "RichTextstyles__ToolbarItems",
|
|
23
|
+
componentId: "sc-8lgglv-4"
|
|
24
|
+
})(["&&{display:flex;align-items:center;flex:1 1 auto;min-width:0;overflow:hidden;}> *{flex:0 0 auto;}"]);
|
|
25
|
+
const ToolbarEntry = /* @__PURE__ */ styled.span.withConfig({
|
|
26
|
+
displayName: "RichTextstyles__ToolbarEntry",
|
|
27
|
+
componentId: "sc-8lgglv-5"
|
|
28
|
+
})(["display:flex;align-items:center;flex:0 0 auto;"]);
|
|
29
|
+
const EditorArea = /* @__PURE__ */ styled.div.withConfig({
|
|
30
|
+
displayName: "RichTextstyles__EditorArea",
|
|
31
|
+
componentId: "sc-8lgglv-6"
|
|
32
|
+
})([".ProseMirror{padding:12px;outline:none;min-height:", ";height:", ";max-height:", ";overflow-y:auto;font-size:13px;line-height:1.6;color:", ";", ";> * + *{margin-top:0.5em;}h1{font-size:24px;font-weight:500;line-height:1.3;margin:0 0 0.5em;}h2{font-size:20px;font-weight:500;line-height:1.3;margin:0 0 0.5em;}h3{font-size:16px;font-weight:500;line-height:1.3;margin:0 0 0.5em;}p{margin:0 0 4px;}ul,ol{padding-left:24px;margin:0 0 4px;}li{margin:2px 0;}a{color:", ";text-decoration:underline;cursor:pointer;}code{background:", ";padding:2px 4px;border-radius:3px;font-family:'SFMono-Regular',Consolas,'Liberation Mono',Menlo,monospace;font-size:12px;}pre{background:", ";padding:12px;border-radius:3px;overflow-x:auto;code{background:none;padding:0;border-radius:0;font-size:12px;}}blockquote{border-left:3px solid ", ";padding-left:12px;margin:0 0 4px;color:", ";}.ds-rt-code-snippet{position:relative;display:flex;align-items:center;background:", ";border-radius:3px;padding:6px 40px 6px 12px;margin:0 0 4px;code{flex:1 1 auto;background:none;padding:0;border-radius:0;white-space:pre;overflow-x:auto;outline:none;}.ds-rt-code-snippet__actions{position:absolute;top:50%;right:4px;transform:translateY(-50%);}}table{border-collapse:collapse;width:100%;margin:0 0 4px;table-layout:fixed;overflow:hidden;td,th{border:1px solid ", ";padding:6px 10px;vertical-align:top;text-align:left;position:relative;> *{margin:0;}}th{background:", ";font-weight:500;}.selectedCell::after{content:'';position:absolute;inset:0;background:", ";opacity:0.4;pointer-events:none;}}s{text-decoration:line-through;}img{max-width:100%;height:auto;border-radius:3px;display:block;margin:8px 0;&.ProseMirror-selectednode{outline:2px solid ", ";outline-offset:2px;}}}.ProseMirror p.is-editor-empty:first-child::before{color:", ";content:attr(data-placeholder);float:left;height:0;pointer-events:none;}"], (props) => props.$subtlePreview ? "32px" : "120px", (props) => props.$height ? typeof props.$height === "number" ? `${props.$height}px` : props.$height : "auto", (props) => props.$maxHeight ? typeof props.$maxHeight === "number" ? `${props.$maxHeight}px` : props.$maxHeight : "none", (props) => props.theme.palette["grey-800"], (props) => props.$readOnly && css(["background:", ";cursor:default;"], props.theme.palette["grey-050"]), (props) => props.theme.palette["blue-600"], (props) => props.theme.palette["grey-100"], (props) => props.theme.palette["grey-100"], (props) => props.theme.palette["grey-300"], (props) => props.theme.palette["grey-500"], (props) => props.theme.palette["grey-050"], (props) => props.theme.palette["grey-300"], (props) => props.theme.palette["grey-050"], (props) => props.theme.palette["blue-050"], (props) => props.theme.palette["blue-600"], (props) => props.theme.palette["grey-400"]);
|
|
33
|
+
const ContentAbove = /* @__PURE__ */ styled.div.withConfig({
|
|
34
|
+
displayName: "RichTextstyles__ContentAbove",
|
|
35
|
+
componentId: "sc-8lgglv-7"
|
|
36
|
+
})(["margin-bottom:8px;"]);
|
|
37
|
+
const ContentBelow = /* @__PURE__ */ styled.div.withConfig({
|
|
38
|
+
displayName: "RichTextstyles__ContentBelow",
|
|
39
|
+
componentId: "sc-8lgglv-8"
|
|
40
|
+
})(["margin-top:8px;"]);
|
|
41
|
+
const ErrorText = /* @__PURE__ */ styled.div.withConfig({
|
|
42
|
+
displayName: "RichTextstyles__ErrorText",
|
|
43
|
+
componentId: "sc-8lgglv-9"
|
|
44
|
+
})(["color:", ";font-size:13px;line-height:1.39;"], (props) => props.theme.palette["red-600"]);
|
|
45
|
+
const Description = /* @__PURE__ */ styled.div.withConfig({
|
|
46
|
+
displayName: "RichTextstyles__Description",
|
|
47
|
+
componentId: "sc-8lgglv-10"
|
|
48
|
+
})(["color:", ";font-size:13px;line-height:1.39;"], (props) => props.theme.palette["grey-500"]);
|
|
49
|
+
const AiGradientLabel = /* @__PURE__ */ styled.span.withConfig({
|
|
50
|
+
displayName: "RichTextstyles__AiGradientLabel",
|
|
51
|
+
componentId: "sc-8lgglv-11"
|
|
52
|
+
})(["background:linear-gradient( 90deg,", " 0%,", " 100% );-webkit-background-clip:text;background-clip:text;-webkit-text-fill-color:transparent;"], (props) => props.theme.palette["mars-500"], (props) => props.theme.palette["purple-400"]);
|
|
53
|
+
const ToolbarSeparator = /* @__PURE__ */ styled.div.withConfig({
|
|
54
|
+
displayName: "RichTextstyles__ToolbarSeparator",
|
|
55
|
+
componentId: "sc-8lgglv-12"
|
|
56
|
+
})(["&&{align-self:center;flex:0 0 auto;width:1px;height:18px;min-height:18px;margin:0 4px;padding:0;border-radius:0;background:", ";}"], (props) => props.theme.palette["grey-200"]);
|
|
57
|
+
const popoverSurface = /* @__PURE__ */ css(["background:", ";border:1px solid ", ";border-radius:3px;box-shadow:0 4px 12px 0 rgba(0,0,0,0.12);"], (props) => props.theme.palette.white, (props) => props.theme.palette["grey-200"]);
|
|
58
|
+
const BlockMenu = /* @__PURE__ */ styled.div.withConfig({
|
|
59
|
+
displayName: "RichTextstyles__BlockMenu",
|
|
60
|
+
componentId: "sc-8lgglv-13"
|
|
61
|
+
})(["", ";display:flex;flex-direction:column;gap:2px;padding:4px;min-width:160px;.ds-button{justify-content:flex-start;}.ds-button-label{justify-content:flex-start;}"], popoverSurface);
|
|
62
|
+
const PopoverForm = /* @__PURE__ */ styled.div.withConfig({
|
|
63
|
+
displayName: "RichTextstyles__PopoverForm",
|
|
64
|
+
componentId: "sc-8lgglv-14"
|
|
65
|
+
})(["", ";display:flex;flex-direction:column;gap:8px;padding:12px;min-width:280px;"], popoverSurface);
|
|
66
|
+
const PopoverActions = /* @__PURE__ */ styled.div.withConfig({
|
|
67
|
+
displayName: "RichTextstyles__PopoverActions",
|
|
68
|
+
componentId: "sc-8lgglv-15"
|
|
69
|
+
})(["display:flex;gap:8px;"]);
|
|
70
|
+
export {
|
|
71
|
+
AiGradientLabel,
|
|
72
|
+
BlockMenu,
|
|
73
|
+
ContentAbove,
|
|
74
|
+
ContentBelow,
|
|
75
|
+
Description,
|
|
76
|
+
EditorArea,
|
|
77
|
+
EditorWrapper,
|
|
78
|
+
ErrorText,
|
|
79
|
+
PopoverActions,
|
|
80
|
+
PopoverForm,
|
|
81
|
+
RichTextWrapper,
|
|
82
|
+
SubtleSuffix,
|
|
83
|
+
ToolbarArea,
|
|
84
|
+
ToolbarEntry,
|
|
85
|
+
ToolbarItems,
|
|
86
|
+
ToolbarSeparator
|
|
87
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { CSSProperties, ReactNode } from 'react';
|
|
2
|
+
import { TooltipProps } from '@synerise/ds-tooltip';
|
|
3
|
+
import { Editor } from '@tiptap/react';
|
|
4
|
+
import { RichTextDocument } from './blockModel';
|
|
5
|
+
export type RichTextTexts = {
|
|
6
|
+
paragraph: string;
|
|
7
|
+
bold: string;
|
|
8
|
+
italic: string;
|
|
9
|
+
underline: string;
|
|
10
|
+
strikethrough: string;
|
|
11
|
+
heading: string;
|
|
12
|
+
bulletList: string;
|
|
13
|
+
orderedList: string;
|
|
14
|
+
link: string;
|
|
15
|
+
align: string;
|
|
16
|
+
alignLeft: string;
|
|
17
|
+
alignCenter: string;
|
|
18
|
+
alignRight: string;
|
|
19
|
+
codeBlock: string;
|
|
20
|
+
codeSnippet: string;
|
|
21
|
+
copySnippet: string;
|
|
22
|
+
copiedSnippet: string;
|
|
23
|
+
editWithAI: string;
|
|
24
|
+
more: string;
|
|
25
|
+
table: string;
|
|
26
|
+
insertTable: string;
|
|
27
|
+
addRowBefore: string;
|
|
28
|
+
addRowAfter: string;
|
|
29
|
+
addColumnBefore: string;
|
|
30
|
+
addColumnAfter: string;
|
|
31
|
+
deleteRow: string;
|
|
32
|
+
deleteColumn: string;
|
|
33
|
+
deleteTable: string;
|
|
34
|
+
linkPlaceholder: string;
|
|
35
|
+
linkConfirm: string;
|
|
36
|
+
linkRemove: string;
|
|
37
|
+
image: string;
|
|
38
|
+
imagePlaceholder: string;
|
|
39
|
+
imageConfirm: string;
|
|
40
|
+
imageUpload: string;
|
|
41
|
+
};
|
|
42
|
+
export type ToolbarFeature = 'bold' | 'italic' | 'underline' | 'strikethrough' | 'heading' | 'bulletList' | 'orderedList' | 'link' | 'textAlign' | 'codeBlock' | 'codeSnippet' | 'table' | 'image';
|
|
43
|
+
export declare const ALL_TOOLBAR_FEATURES: ToolbarFeature[];
|
|
44
|
+
export declare const DEFAULT_HEADING_LEVELS: (1 | 2 | 3 | 4 | 5 | 6)[];
|
|
45
|
+
export declare const DEFAULT_TEXTS: RichTextTexts;
|
|
46
|
+
export type RichTextFormat = 'html' | 'document' | 'markdown';
|
|
47
|
+
export type RichTextAIOption = {
|
|
48
|
+
key: string;
|
|
49
|
+
label: ReactNode;
|
|
50
|
+
/** Rendered as the ds-button `icon` — pass e.g. `<Icon component={<AiStarM />} />`. */
|
|
51
|
+
icon?: ReactNode;
|
|
52
|
+
/**
|
|
53
|
+
* Called with the current selection (or the full text when nothing is
|
|
54
|
+
* selected). A resolved string replaces the selection; resolving with
|
|
55
|
+
* null/undefined leaves the content unchanged (when the consumer handles
|
|
56
|
+
* the action itself, e.g. opens its own dialog).
|
|
57
|
+
*/
|
|
58
|
+
onSelect: (selectedText: string) => Promise<string | null | void> | string | null | void;
|
|
59
|
+
};
|
|
60
|
+
export type RichTextProps = {
|
|
61
|
+
/**
|
|
62
|
+
* Output format for `value` / `defaultValue` / `onChange`:
|
|
63
|
+
* - `'html'` (default) — HTML string
|
|
64
|
+
* - `'document'` — `RichTextDocument` (JSON block model)
|
|
65
|
+
* - `'markdown'` — Markdown string
|
|
66
|
+
*/
|
|
67
|
+
format?: RichTextFormat;
|
|
68
|
+
value?: string | RichTextDocument;
|
|
69
|
+
defaultValue?: string | RichTextDocument;
|
|
70
|
+
onChange?: (value: string | RichTextDocument) => void;
|
|
71
|
+
onEditorReady?: (editor: Editor) => void;
|
|
72
|
+
placeholder?: string;
|
|
73
|
+
readOnly?: boolean;
|
|
74
|
+
disabled?: boolean;
|
|
75
|
+
label?: ReactNode;
|
|
76
|
+
tooltip?: ReactNode;
|
|
77
|
+
tooltipConfig?: TooltipProps;
|
|
78
|
+
description?: ReactNode;
|
|
79
|
+
errorText?: ReactNode;
|
|
80
|
+
height?: string | number;
|
|
81
|
+
maxHeight?: string | number;
|
|
82
|
+
toolbarFeatures?: ToolbarFeature[];
|
|
83
|
+
hideToolbar?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Subtle (inline-edit) mode, like SubtleForm components. The editor starts as a
|
|
86
|
+
* borderless preview; hovering shows a grey background and an edit icon, clicking
|
|
87
|
+
* anywhere on the field switches to edit mode (toolbar + border), and clicking
|
|
88
|
+
* outside returns to preview. `errorText` forces edit mode.
|
|
89
|
+
*/
|
|
90
|
+
subtle?: boolean;
|
|
91
|
+
headingLevels?: (1 | 2 | 3 | 4 | 5 | 6)[];
|
|
92
|
+
texts?: Partial<RichTextTexts>;
|
|
93
|
+
noBorder?: boolean;
|
|
94
|
+
style?: CSSProperties;
|
|
95
|
+
className?: string;
|
|
96
|
+
autoFocus?: boolean;
|
|
97
|
+
onImageUpload?: (file: File) => string | Promise<string>;
|
|
98
|
+
/**
|
|
99
|
+
* AI "Edit with AI" handler. When provided, the toolbar shows an "Edit with AI" button;
|
|
100
|
+
* on click the current selection (or full text when nothing is selected, possibly an
|
|
101
|
+
* empty string) is passed in and the resolved string replaces it. Resolving with
|
|
102
|
+
* null/undefined leaves the content unchanged, so the handler can drive an external
|
|
103
|
+
* flow (e.g. a platform agent) instead. The design system performs no AI call itself.
|
|
104
|
+
*/
|
|
105
|
+
onEditWithAI?: (selectedText: string) => Promise<string | null | undefined>;
|
|
106
|
+
/**
|
|
107
|
+
* When provided (non-empty), the "Edit with AI" button opens a dropdown with
|
|
108
|
+
* these options instead of calling `onEditWithAI` directly. Options and their
|
|
109
|
+
* behavior are fully defined by the consumer.
|
|
110
|
+
*/
|
|
111
|
+
editWithAIOptions?: RichTextAIOption[];
|
|
112
|
+
/**
|
|
113
|
+
* Enables the SubtleForm-style transitions in `subtle` mode (content sliding
|
|
114
|
+
* right on hover, placeholder dropping into place, blur compensation).
|
|
115
|
+
* When disabled (default), hovering the preview only tints the background.
|
|
116
|
+
*/
|
|
117
|
+
animations?: boolean;
|
|
118
|
+
};
|
|
119
|
+
export type ToolbarProps = {
|
|
120
|
+
editor: Editor | null;
|
|
121
|
+
features: ToolbarFeature[];
|
|
122
|
+
headingLevels: (1 | 2 | 3 | 4 | 5 | 6)[];
|
|
123
|
+
texts: RichTextTexts;
|
|
124
|
+
disabled?: boolean;
|
|
125
|
+
onImageUpload?: (file: File) => string | Promise<string>;
|
|
126
|
+
onEditWithAI?: (selectedText: string) => Promise<string | null | undefined>;
|
|
127
|
+
editWithAIOptions?: RichTextAIOption[];
|
|
128
|
+
};
|
|
129
|
+
export type LinkPopoverProps = {
|
|
130
|
+
editor: Editor;
|
|
131
|
+
texts: RichTextTexts;
|
|
132
|
+
onClose: () => void;
|
|
133
|
+
};
|
|
134
|
+
export type ImagePopoverProps = {
|
|
135
|
+
editor: Editor;
|
|
136
|
+
texts: RichTextTexts;
|
|
137
|
+
onClose: () => void;
|
|
138
|
+
onImageUpload?: (file: File) => string | Promise<string>;
|
|
139
|
+
};
|