@lowdefy/blocks-tiptap 0.0.0-experimental-20260428103147

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,96 @@
1
+ @import '../utils/style.module.css';
2
+
3
+ :global(.tiptap-mention) {
4
+ box-decoration-break: clone;
5
+ font-weight: 600;
6
+ color: var(--ant-color-link);
7
+ }
8
+
9
+ :global(.tiptap-mention-items) {
10
+ background: var(--ant-color-bg-container);
11
+ border-radius: var(--ant-border-radius, 8px);
12
+ box-shadow:
13
+ 0 0 0 1px rgba(0, 0, 0, 0.05),
14
+ 0px 10px 20px rgba(0, 0, 0, 0.1);
15
+ color: var(--ant-color-text);
16
+ font-size: var(--ant-font-size);
17
+ overflow: hidden;
18
+ padding: 0.2rem;
19
+ position: relative;
20
+ }
21
+
22
+ :global(.tiptap-mention-item) {
23
+ background: transparent;
24
+ border: 1px solid transparent;
25
+ border-radius: var(--ant-border-radius, 8px);
26
+ display: block;
27
+ margin: 0;
28
+ padding: 0.2rem 0.4rem;
29
+ text-align: left;
30
+ line-height: normal;
31
+ width: 100%;
32
+ }
33
+
34
+ :global(.tiptap-mention-item.is-selected) {
35
+ border-color: var(--ant-color-primary);
36
+ background: var(--ant-control-item-bg-hover);
37
+ }
38
+
39
+ :global(.tableWrapper),
40
+ :global(.tiptap-table) {
41
+ display: block;
42
+ overflow-x: auto;
43
+ max-width: 80%;
44
+ }
45
+
46
+ :global(.tableWrapper) :global(table),
47
+ :global(.tiptap-table) :global(table) {
48
+ border-collapse: collapse;
49
+ width: 100%;
50
+ min-width: 75px;
51
+ }
52
+
53
+ :global(.tableWrapper) :global(colgroup) :global(col),
54
+ :global(.tiptap-table) :global(colgroup) :global(col) {
55
+ width: auto;
56
+ }
57
+
58
+ :global(.tableWrapper) :global(table),
59
+ :global(.tableWrapper) :global(th),
60
+ :global(.tableWrapper) :global(td),
61
+ :global(.tiptap-table) :global(table),
62
+ :global(.tiptap-table) :global(th),
63
+ :global(.tiptap-table) :global(td) {
64
+ border: 1px solid var(--ant-color-border);
65
+ }
66
+
67
+ :global(.tableWrapper) :global(th),
68
+ :global(.tableWrapper) :global(td),
69
+ :global(.tiptap-table) :global(th),
70
+ :global(.tiptap-table) :global(td) {
71
+ padding: 2px;
72
+ text-align: left;
73
+ }
74
+
75
+ :global(.tableWrapper) :global(th),
76
+ :global(.tiptap-table) :global(th) {
77
+ background-color: var(--ant-color-fill-tertiary);
78
+ font-weight: bold;
79
+ }
80
+
81
+ :global(.tableWrapper) :global(tr):nth-child(even),
82
+ :global(.tiptap-table) :global(tr):nth-child(even) {
83
+ background-color: var(--ant-color-fill-quaternary);
84
+ }
85
+
86
+ :global(.tableWrapper) :global(tr):hover,
87
+ :global(.tiptap-table) :global(tr):hover {
88
+ background-color: var(--ant-color-primary-bg);
89
+ }
90
+
91
+ :global(.tableWrapper) :global(td) :global(p),
92
+ :global(.tableWrapper) :global(th) :global(p),
93
+ :global(.tiptap-table) :global(td) :global(p),
94
+ :global(.tiptap-table) :global(th) :global(p) {
95
+ margin: 0;
96
+ }
@@ -0,0 +1,82 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { ReactRenderer } from '@tiptap/react';
16
+ import { type } from '@lowdefy/helpers';
17
+ import tippy from 'tippy.js';
18
+ import MentionList from './MentionList.js';
19
+ function suggestion({ methods, char = '@', allowSpaces = true }) {
20
+ return {
21
+ // `char` must be set explicitly: Mention.configure does a shallow merge
22
+ // on its options.suggestion, replacing the extension's default `@`.
23
+ char,
24
+ allowSpaces,
25
+ items: ({ query, editor })=>{
26
+ const itemsList = editor.options.editorProps.items ?? [];
27
+ return itemsList.filter((item)=>{
28
+ if (type.isString(item)) {
29
+ return item.toLowerCase().includes(query.toLowerCase());
30
+ }
31
+ if (type.isString(item?.label)) {
32
+ return item.label.toLowerCase().includes(query.toLowerCase());
33
+ }
34
+ return false;
35
+ }).slice(0, 5);
36
+ },
37
+ render: ()=>{
38
+ let component;
39
+ let popup;
40
+ return {
41
+ onStart: (props)=>{
42
+ component = new ReactRenderer(MentionList, {
43
+ props: {
44
+ ...props,
45
+ methods
46
+ },
47
+ editor: props.editor
48
+ });
49
+ if (!props.clientRect) return;
50
+ popup = tippy('body', {
51
+ getReferenceClientRect: props.clientRect,
52
+ appendTo: ()=>document.body,
53
+ content: component.element,
54
+ showOnCreate: true,
55
+ interactive: true,
56
+ trigger: 'manual',
57
+ placement: 'bottom-start'
58
+ });
59
+ },
60
+ onUpdate (props) {
61
+ component.updateProps(props);
62
+ if (!props.clientRect) return;
63
+ popup[0].setProps({
64
+ getReferenceClientRect: props.clientRect
65
+ });
66
+ },
67
+ onKeyDown (props) {
68
+ if (props.event.key === 'Escape') {
69
+ popup[0].hide();
70
+ return true;
71
+ }
72
+ return component.ref?.onKeyDown(props);
73
+ },
74
+ onExit () {
75
+ popup[0].destroy();
76
+ component.destroy();
77
+ }
78
+ };
79
+ }
80
+ };
81
+ }
82
+ export default suggestion;
@@ -0,0 +1,108 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { useRef } from 'react';
16
+ import TurndownService from 'turndown';
17
+ import s3FileUpload from '../utils/s3FileUpload.js';
18
+ // Ref-tracked controller for the mention editor's value. Mirrors TiptapInput's
19
+ // useTiptapState but also extracts mentions from the document.
20
+ function useTiptapMentionState({ value, methods }) {
21
+ const valueRef = useRef(value);
22
+ valueRef.current = value;
23
+ const turndownService = new TurndownService();
24
+ turndownService.addRule('encodeImgUrl', {
25
+ filter: 'img',
26
+ replacement: (content, node)=>{
27
+ const src = node.getAttribute('src');
28
+ return `![${content}](${encodeURI(src)})`;
29
+ }
30
+ });
31
+ const emit = (editor, appendFile)=>{
32
+ const html = editor.getHTML();
33
+ const markdown = turndownService.turndown(html);
34
+ const json = editor.getJSON();
35
+ const text = editor.getText().trim() === '' ? null : editor.getText();
36
+ const urls = (json.content ?? []).filter((c)=>c.type === 'image').map((c)=>c.attrs.src);
37
+ const base = valueRef.current?.fileList ?? [];
38
+ const next = appendFile ? [
39
+ ...base,
40
+ appendFile
41
+ ] : base;
42
+ const fileList = next.filter((f)=>urls.includes(f.url));
43
+ const mentionsMap = new Map();
44
+ (json.content ?? []).filter((c)=>c.type === 'paragraph').forEach((c)=>{
45
+ c.content?.forEach((cc)=>{
46
+ if (cc.type === 'mention') {
47
+ mentionsMap.set(JSON.stringify(cc.attrs.id.value), cc.attrs.id.value);
48
+ }
49
+ });
50
+ });
51
+ const mentions = Array.from(mentionsMap.values());
52
+ methods.setValue({
53
+ fileList,
54
+ html,
55
+ mentions,
56
+ text,
57
+ markdown
58
+ });
59
+ };
60
+ const insertImage = async (editor, file, pos)=>{
61
+ const url = await s3FileUpload({
62
+ file,
63
+ methods
64
+ });
65
+ editor.chain().insertContentAt(pos, [
66
+ {
67
+ type: 'image',
68
+ attrs: {
69
+ src: url
70
+ }
71
+ },
72
+ {
73
+ type: 'paragraph',
74
+ content: [
75
+ {
76
+ type: 'text',
77
+ marks: [
78
+ {
79
+ type: 'link',
80
+ attrs: {
81
+ href: url,
82
+ target: '_blank'
83
+ }
84
+ }
85
+ ],
86
+ text: 'Enlarge Image'
87
+ }
88
+ ]
89
+ }
90
+ ]).focus().run();
91
+ const fileObj = {
92
+ bucket: file.bucket,
93
+ key: file.key,
94
+ lastModified: file.lastModified,
95
+ name: file.name,
96
+ size: file.size,
97
+ status: file.status,
98
+ type: file.type,
99
+ url
100
+ };
101
+ emit(editor, fileObj);
102
+ };
103
+ return {
104
+ emit,
105
+ insertImage
106
+ };
107
+ }
108
+ export default useTiptapMentionState;
@@ -0,0 +1,82 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import React from 'react';
16
+ import { BubbleMenu } from '@tiptap/react';
17
+ import { AiOutlineBold, AiOutlineItalic, AiOutlineStrikethrough, AiOutlineHighlight } from 'react-icons/ai';
18
+ import { isTextSelection } from '@tiptap/core';
19
+ const HIGHLIGHT_SWATCHES = [
20
+ {
21
+ color: 'rgba(170, 255, 0, 1)',
22
+ fill: 'rgba(170, 255, 0, 0.5)'
23
+ },
24
+ {
25
+ color: 'rgba(255, 170, 0, 1)',
26
+ fill: 'rgba(255, 170, 0, 0.5)'
27
+ },
28
+ {
29
+ color: 'rgba(255, 0, 170, 1)',
30
+ fill: 'rgba(255, 0, 170, 0.5)'
31
+ },
32
+ {
33
+ color: 'rgba(170, 0, 255, 1)',
34
+ fill: 'rgba(170, 0, 255, 0.5)'
35
+ }
36
+ ];
37
+ function hasExt(editor, name) {
38
+ return editor.extensionManager.extensions.some((ext)=>ext.name === name);
39
+ }
40
+ const PopoverMenu = ({ editor })=>{
41
+ const showBold = hasExt(editor, 'bold');
42
+ const showItalic = hasExt(editor, 'italic');
43
+ const showStrike = hasExt(editor, 'strike');
44
+ const showHighlight = hasExt(editor, 'highlight');
45
+ if (!showBold && !showItalic && !showStrike && !showHighlight) {
46
+ return null;
47
+ }
48
+ return /*#__PURE__*/ React.createElement(BubbleMenu, {
49
+ className: "tiptap-popover",
50
+ editor: editor,
51
+ shouldShow: ({ editor, view, state, from, to })=>{
52
+ if (editor.isActive('image')) return false;
53
+ const { doc, selection } = state;
54
+ const { empty } = selection;
55
+ const isEmptyTextBlock = !doc.textBetween(from, to).length && isTextSelection(state.selection);
56
+ const hasEditorFocus = view.hasFocus();
57
+ if (!hasEditorFocus || empty || isEmptyTextBlock || !editor.isEditable) {
58
+ return false;
59
+ }
60
+ return true;
61
+ }
62
+ }, showBold && /*#__PURE__*/ React.createElement(AiOutlineBold, {
63
+ className: "tiptap-icon",
64
+ onClick: ()=>editor.chain().focus().toggleBold().run()
65
+ }), showItalic && /*#__PURE__*/ React.createElement(AiOutlineItalic, {
66
+ className: "tiptap-icon",
67
+ onClick: ()=>editor.chain().focus().toggleItalic().run()
68
+ }), showStrike && /*#__PURE__*/ React.createElement(AiOutlineStrikethrough, {
69
+ className: "tiptap-icon",
70
+ onClick: ()=>editor.chain().focus().toggleStrike().run()
71
+ }), showHighlight && HIGHLIGHT_SWATCHES.map(({ color, fill })=>/*#__PURE__*/ React.createElement(AiOutlineHighlight, {
72
+ key: color,
73
+ className: "tiptap-icon",
74
+ style: {
75
+ color
76
+ },
77
+ onClick: ()=>editor.chain().focus().toggleHighlight({
78
+ color: fill
79
+ }).run()
80
+ })));
81
+ };
82
+ export default PopoverMenu;
@@ -0,0 +1,127 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import FileHandler from '@tiptap/extension-file-handler';
16
+ import Highlight from '@tiptap/extension-highlight';
17
+ import Image from '@tiptap/extension-image';
18
+ import Placeholder from '@tiptap/extension-placeholder';
19
+ import StarterKit from '@tiptap/starter-kit';
20
+ import LinkExtension from '@tiptap/extension-link';
21
+ import Table from '@tiptap/extension-table';
22
+ import TableCell from '@tiptap/extension-table-cell';
23
+ import TableHeader from '@tiptap/extension-table-header';
24
+ import TableRow from '@tiptap/extension-table-row';
25
+ import { type } from '@lowdefy/helpers';
26
+ const DEFAULT_IMAGE_MIME_TYPES = [
27
+ 'image/jpeg',
28
+ 'image/png',
29
+ 'image/gif',
30
+ 'image/webp'
31
+ ];
32
+ const DEFAULTS = {
33
+ image: {
34
+ disabled: false,
35
+ maxWidth: '80%',
36
+ zoom: 0.5
37
+ },
38
+ table: {
39
+ disabled: false,
40
+ resizable: true
41
+ },
42
+ link: {
43
+ disabled: false,
44
+ openOnClick: true,
45
+ autolink: true,
46
+ linkOnPaste: true,
47
+ defaultProtocol: 'https'
48
+ },
49
+ highlight: {
50
+ disabled: false,
51
+ multicolor: true
52
+ }
53
+ };
54
+ function merge(defaults, overrides) {
55
+ if (!type.isObject(overrides)) return defaults;
56
+ return {
57
+ ...defaults,
58
+ ...overrides
59
+ };
60
+ }
61
+ function buildExtensions({ properties, insertImage, mentionExtension, uploadEnabled }) {
62
+ const image = merge(DEFAULTS.image, properties.image);
63
+ const table = merge(DEFAULTS.table, properties.table);
64
+ const link = merge(DEFAULTS.link, properties.link);
65
+ const highlight = merge(DEFAULTS.highlight, properties.highlight);
66
+ const starterKitOptions = type.isObject(properties.starterKit) ? properties.starterKit : {};
67
+ const allowedMimeTypes = type.isArray(properties.allowedMimeTypes) ? properties.allowedMimeTypes : DEFAULT_IMAGE_MIME_TYPES;
68
+ const extensions = [
69
+ StarterKit.configure(starterKitOptions)
70
+ ];
71
+ if (!table.disabled) {
72
+ extensions.push(Table.configure({
73
+ HTMLAttributes: {
74
+ class: 'tiptap-table'
75
+ },
76
+ resizable: table.resizable
77
+ }), TableRow, TableHeader, TableCell);
78
+ }
79
+ if (!image.disabled) {
80
+ extensions.push(Image.configure({
81
+ HTMLAttributes: {
82
+ style: `max-width: ${image.maxWidth}; display: block; zoom: ${image.zoom};`
83
+ }
84
+ }));
85
+ }
86
+ extensions.push(Placeholder.configure({
87
+ placeholder: ()=>properties.placeholder ?? '',
88
+ showOnlyWhenEditable: false,
89
+ considerAnyAsEmpty: true
90
+ }));
91
+ if (!highlight.disabled) {
92
+ extensions.push(Highlight.configure({
93
+ multicolor: highlight.multicolor,
94
+ HTMLAttributes: {
95
+ style: 'padding: 0;'
96
+ }
97
+ }));
98
+ }
99
+ if (!link.disabled) {
100
+ extensions.push(LinkExtension.configure({
101
+ autolink: link.autolink,
102
+ linkOnPaste: link.linkOnPaste,
103
+ openOnClick: link.openOnClick,
104
+ defaultProtocol: link.defaultProtocol
105
+ }));
106
+ }
107
+ if (mentionExtension) {
108
+ extensions.push(mentionExtension);
109
+ }
110
+ if (uploadEnabled) {
111
+ extensions.push(FileHandler.configure({
112
+ onDrop: (editor, files, pos)=>{
113
+ files.forEach((file)=>insertImage(editor, file, pos));
114
+ },
115
+ onPaste: (editor, files, htmlContent)=>{
116
+ if (htmlContent) return false;
117
+ files.forEach((file)=>{
118
+ const pos = editor.state.selection.anchor;
119
+ insertImage(editor, file, pos);
120
+ });
121
+ },
122
+ allowedMimeTypes
123
+ }));
124
+ }
125
+ return extensions;
126
+ }
127
+ export default buildExtensions;
@@ -0,0 +1,57 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { type } from '@lowdefy/helpers';
16
+ // Matches ant-design Input's line-height ratio at the default font size,
17
+ // plus the 8px top/bottom padding on the ProseMirror editable area.
18
+ const LINE_HEIGHT = '1.5714em';
19
+ const VERTICAL_PADDING = 16;
20
+ function calcRows(n) {
21
+ return `calc(${n} * ${LINE_HEIGHT} + ${VERTICAL_PADDING}px)`;
22
+ }
23
+ const DEFAULT_MIN_ROWS = 5;
24
+ // Translate TextArea-style rows/autoSize props into React inline CSS applied
25
+ // to the EditorContent wrapper. The wrapper becomes the scroll container;
26
+ // ProseMirror's editable inside grows naturally.
27
+ function computeHeightStyle({ rows, autoSize }) {
28
+ if (type.isInt(rows) && rows >= 1) {
29
+ const h = calcRows(rows);
30
+ return {
31
+ minHeight: h,
32
+ maxHeight: h,
33
+ overflowY: 'auto'
34
+ };
35
+ }
36
+ if (autoSize === true) {
37
+ return {
38
+ minHeight: calcRows(1),
39
+ maxHeight: 'none'
40
+ };
41
+ }
42
+ if (type.isObject(autoSize)) {
43
+ const style = {};
44
+ if (type.isInt(autoSize.minRows) && autoSize.minRows >= 1) {
45
+ style.minHeight = calcRows(autoSize.minRows);
46
+ }
47
+ if (type.isInt(autoSize.maxRows) && autoSize.maxRows >= 1) {
48
+ style.maxHeight = calcRows(autoSize.maxRows);
49
+ style.overflowY = 'auto';
50
+ }
51
+ if (Object.keys(style).length > 0) return style;
52
+ }
53
+ return {
54
+ minHeight: calcRows(DEFAULT_MIN_ROWS)
55
+ };
56
+ }
57
+ export default computeHeightStyle;
@@ -0,0 +1,51 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ async function s3FileUpload({ file, methods }) {
16
+ if (!file) {
17
+ return;
18
+ }
19
+ const { lastModified, name, size, type, uid } = file;
20
+ const s3PostPolicyResponse = await methods.triggerEvent({
21
+ name: '__getS3PostPolicy',
22
+ event: {
23
+ file: {
24
+ name,
25
+ lastModified,
26
+ size,
27
+ type,
28
+ uid
29
+ }
30
+ }
31
+ });
32
+ if (s3PostPolicyResponse.success !== true) {
33
+ throw new Error('S3 post policy request error.');
34
+ }
35
+ const { url, fields } = s3PostPolicyResponse.responses.__getS3PostPolicy.response[0];
36
+ const { bucket, key } = fields;
37
+ file.bucket = bucket;
38
+ file.key = key;
39
+ const formData = new FormData();
40
+ Object.keys(fields).forEach((field)=>{
41
+ formData.append(field, fields[field]);
42
+ });
43
+ formData.append('file', file);
44
+ await fetch(url, {
45
+ method: 'POST',
46
+ body: formData
47
+ });
48
+ file.url = `${url}/${key}`;
49
+ return file.url;
50
+ }
51
+ export default s3FileUpload;
@@ -0,0 +1,24 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ // Map validation.status → wrapper class. Matches the visual behavior of
16
+ // passing status="error" | "warning" to an antd <Input />, but rendered
17
+ // with our own CSS (see utils/style.module.css) because antd v6 uses
18
+ // CSS-in-JS, so the .ant-input-status-* classes are not global.
19
+ function statusClass(status) {
20
+ if (status === 'error') return 'tiptap-wrapper-error';
21
+ if (status === 'warning') return 'tiptap-wrapper-warning';
22
+ return '';
23
+ }
24
+ export default statusClass;