@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.
Files changed (52) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE.md +21 -0
  3. package/README.md +173 -0
  4. package/dist/RichText.d.ts +4 -0
  5. package/dist/RichText.js +179 -0
  6. package/dist/RichText.styles.d.ts +31 -0
  7. package/dist/RichText.styles.js +87 -0
  8. package/dist/RichText.types.d.ts +139 -0
  9. package/dist/RichText.types.js +44 -0
  10. package/dist/blockModel/blockModel.types.d.ts +130 -0
  11. package/dist/blockModel/blockModel.types.js +1 -0
  12. package/dist/blockModel/index.d.ts +2 -0
  13. package/dist/blockModel/index.js +14 -0
  14. package/dist/blockModel/serializers/documentToHtml.d.ts +2 -0
  15. package/dist/blockModel/serializers/documentToHtml.js +101 -0
  16. package/dist/blockModel/serializers/documentToMarkdown.d.ts +2 -0
  17. package/dist/blockModel/serializers/documentToMarkdown.js +84 -0
  18. package/dist/blockModel/serializers/documentToTiptap.d.ts +2 -0
  19. package/dist/blockModel/serializers/documentToTiptap.js +133 -0
  20. package/dist/blockModel/serializers/htmlToDocument.d.ts +2 -0
  21. package/dist/blockModel/serializers/htmlToDocument.js +242 -0
  22. package/dist/blockModel/serializers/index.d.ts +6 -0
  23. package/dist/blockModel/serializers/index.js +14 -0
  24. package/dist/blockModel/serializers/markdownToDocument.d.ts +10 -0
  25. package/dist/blockModel/serializers/markdownToDocument.js +11 -0
  26. package/dist/blockModel/serializers/tiptapToDocument.d.ts +2 -0
  27. package/dist/blockModel/serializers/tiptapToDocument.js +189 -0
  28. package/dist/components/ImagePopover.d.ts +3 -0
  29. package/dist/components/ImagePopover.js +89 -0
  30. package/dist/components/LinkPopover.d.ts +3 -0
  31. package/dist/components/LinkPopover.js +57 -0
  32. package/dist/components/Toolbar.d.ts +3 -0
  33. package/dist/components/Toolbar.js +436 -0
  34. package/dist/components/index.d.ts +3 -0
  35. package/dist/components/index.js +8 -0
  36. package/dist/extensions/CodeSnippet.d.ts +19 -0
  37. package/dist/extensions/CodeSnippet.js +76 -0
  38. package/dist/index.d.ts +6 -0
  39. package/dist/index.js +52 -0
  40. package/dist/modules.d.js +1 -0
  41. package/dist/modules.d.ts +4 -0
  42. package/dist/utils/computeVisibleCount.d.ts +6 -0
  43. package/dist/utils/computeVisibleCount.js +19 -0
  44. package/dist/utils/getExtensions.d.ts +13 -0
  45. package/dist/utils/getExtensions.js +80 -0
  46. package/dist/utils/index.d.ts +3 -0
  47. package/dist/utils/index.js +8 -0
  48. package/dist/utils/normalizeUrl.d.ts +7 -0
  49. package/dist/utils/normalizeUrl.js +13 -0
  50. package/dist/utils/sanitizeUrl.d.ts +9 -0
  51. package/dist/utils/sanitizeUrl.js +30 -0
  52. 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
+
@@ -0,0 +1,4 @@
1
+ import { default as React } from 'react';
2
+ import { RichTextProps } from './RichText.types';
3
+ declare const RichText: React.ForwardRefExoticComponent<RichTextProps & React.RefAttributes<HTMLDivElement>>;
4
+ export default RichText;
@@ -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
+ };