@airoom/nextmin-react 1.4.0 → 1.4.1
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/dist/components/SchemaForm.js +15 -2
- package/dist/components/SchemaSuggestionList.d.ts +7 -0
- package/dist/components/SchemaSuggestionList.js +43 -0
- package/dist/components/editor/TiptapEditor.d.ts +10 -0
- package/dist/components/editor/TiptapEditor.js +228 -0
- package/dist/components/editor/Toolbar.d.ts +6 -0
- package/dist/components/editor/Toolbar.js +99 -0
- package/dist/components/editor/components/CommandList.d.ts +7 -0
- package/dist/components/editor/components/CommandList.js +47 -0
- package/dist/components/editor/components/ImageBubbleMenu.d.ts +6 -0
- package/dist/components/editor/components/ImageBubbleMenu.js +15 -0
- package/dist/components/editor/components/ImageComponent.d.ts +3 -0
- package/dist/components/editor/components/ImageComponent.js +45 -0
- package/dist/components/editor/components/SchemaInsertionModal.d.ts +14 -0
- package/dist/components/editor/components/SchemaInsertionModal.js +38 -0
- package/dist/components/editor/components/TableBubbleMenu.d.ts +6 -0
- package/dist/components/editor/components/TableBubbleMenu.js +8 -0
- package/dist/components/editor/extensions/Container.d.ts +2 -0
- package/dist/components/editor/extensions/Container.js +51 -0
- package/dist/components/editor/extensions/Grid.d.ts +3 -0
- package/dist/components/editor/extensions/Grid.js +89 -0
- package/dist/components/editor/extensions/Layout.d.ts +3 -0
- package/dist/components/editor/extensions/Layout.js +116 -0
- package/dist/components/editor/extensions/ResizableImage.d.ts +1 -0
- package/dist/components/editor/extensions/ResizableImage.js +52 -0
- package/dist/components/editor/extensions/SlashCommand.d.ts +15 -0
- package/dist/components/editor/extensions/SlashCommand.js +161 -0
- package/dist/components/editor/utils/upload.d.ts +1 -0
- package/dist/components/editor/utils/upload.js +49 -0
- package/dist/editor.css +460 -0
- package/dist/lib/upload.d.ts +1 -0
- package/dist/lib/upload.js +53 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +5 -0
- package/dist/nextmin.css +1 -1
- package/dist/router/NextMinRouter.d.ts +1 -0
- package/dist/router/NextMinRouter.js +1 -0
- package/dist/views/list/useListData.js +4 -0
- package/package.json +34 -8
- package/tsconfig.json +8 -3
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button,
|
|
5
|
+
// RadioGroup, // Removed
|
|
6
|
+
// Radio, // Removed
|
|
7
|
+
Input, } from '@heroui/react';
|
|
8
|
+
import { RefMultiSelect } from '../../RefMultiSelect';
|
|
9
|
+
import { cn } from '../../../lib/utils'; // Assuming cn utility is available
|
|
10
|
+
export const SchemaInsertionModal = ({ isOpen, onClose, onInsert, schema, }) => {
|
|
11
|
+
const [viewType, setViewType] = useState('grid');
|
|
12
|
+
const [template, setTemplate] = useState('');
|
|
13
|
+
const [urlPattern, setUrlPattern] = useState('{slug}_{id}');
|
|
14
|
+
const [selectedIds, setSelectedIds] = useState([]);
|
|
15
|
+
// const [limit, setLimit] = useState('6'); // Removed limit
|
|
16
|
+
const handleInsert = () => {
|
|
17
|
+
onInsert({ viewType, template, ids: selectedIds, urlPattern });
|
|
18
|
+
onClose();
|
|
19
|
+
// Reset state
|
|
20
|
+
setTimeout(() => {
|
|
21
|
+
setViewType('grid');
|
|
22
|
+
setTemplate('');
|
|
23
|
+
setUrlPattern('{slug}_{id}');
|
|
24
|
+
setSelectedIds([]);
|
|
25
|
+
}, 200);
|
|
26
|
+
};
|
|
27
|
+
if (!schema)
|
|
28
|
+
return null;
|
|
29
|
+
return (_jsx(Modal, { isOpen: isOpen, onClose: onClose, size: "2xl", classNames: {
|
|
30
|
+
base: "bg-white dark:bg-zinc-950 border border-gray-200 dark:border-gray-800",
|
|
31
|
+
header: "border-b border-gray-200 dark:border-gray-800",
|
|
32
|
+
footer: "border-t border-gray-200 dark:border-gray-800",
|
|
33
|
+
}, children: _jsx(ModalContent, { children: _jsxs(_Fragment, { children: [_jsxs(ModalHeader, { children: ["Insert ", schema.modelName] }), _jsxs(ModalBody, { className: "py-6 flex flex-col gap-6", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("label", { className: "text-sm font-medium", children: "Select Items" }), _jsx(RefMultiSelect, { name: "items", label: "Search & Select", refModel: schema.modelName, value: selectedIds, onChange: (ids) => setSelectedIds(ids), pageSize: 20 }), _jsx("p", { className: "text-xs text-gray-400", children: "Leave empty to fetch latest items automatically." })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("label", { className: "text-sm font-medium", children: "View Type" }), _jsxs("div", { className: "flex gap-4", children: [_jsx("button", { onClick: () => setViewType('grid'), className: cn("px-4 py-2 rounded-lg border text-sm font-medium transition-colors", viewType === 'grid'
|
|
34
|
+
? "bg-primary-50 border-primary-500 text-primary-700 dark:bg-primary-900/20 dark:text-primary-400"
|
|
35
|
+
: "bg-transparent border-gray-200 dark:border-gray-800 hover:bg-gray-50"), children: "Grid (Pills)" }), _jsx("button", { onClick: () => setViewType('list'), className: cn("px-4 py-2 rounded-lg border text-sm font-medium transition-colors", viewType === 'list'
|
|
36
|
+
? "bg-primary-50 border-primary-500 text-primary-700 dark:bg-primary-900/20 dark:text-primary-400"
|
|
37
|
+
: "bg-transparent border-gray-200 dark:border-gray-800 hover:bg-gray-50"), children: "List (Rows)" })] })] }), _jsx(Input, { label: "Template String", placeholder: "e.g. {name} - {designation}", value: template, onChange: (e) => setTemplate(e.target.value), description: "Use {fieldName} to insert dynamic data", variant: "bordered", labelPlacement: "outside" }), _jsx(Input, { label: "URL Suffix", placeholder: "{slug}_{id}", value: urlPattern, onChange: (e) => setUrlPattern(e.target.value), description: `Appended to ${process.env.NEXT_PUBLIC_FRONTEND_URL || 'BASE_URL'}/...`, variant: "bordered", labelPlacement: "outside" })] }), _jsxs(ModalFooter, { children: [_jsx(Button, { variant: "flat", onPress: onClose, children: "Cancel" }), _jsx(Button, { color: "primary", onPress: handleInsert, children: "Insert" })] })] }) }) }));
|
|
38
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { BubbleMenu } from '@tiptap/react/menus';
|
|
3
|
+
import { Columns, Rows, Trash2 } from 'lucide-react';
|
|
4
|
+
export const TableBubbleMenu = ({ editor }) => {
|
|
5
|
+
if (!editor)
|
|
6
|
+
return null;
|
|
7
|
+
return (_jsxs(BubbleMenu, { editor: editor, shouldShow: ({ editor }) => editor.isActive('table'), className: "flex items-center gap-1 bg-white dark:bg-zinc-800 border border-gray-200 dark:border-gray-700 shadow-lg p-1 rounded-lg", children: [_jsxs("button", { type: "button", onClick: () => editor.chain().focus().addColumnBefore().run(), className: "p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200", title: "Add Column Before", children: [_jsx(Columns, { size: 14, className: "rotate-180" }), "+"] }), _jsxs("button", { type: "button", onClick: () => editor.chain().focus().addColumnAfter().run(), className: "p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200", title: "Add Column After", children: [_jsx(Columns, { size: 14 }), "+"] }), _jsxs("button", { type: "button", onClick: () => editor.chain().focus().deleteColumn().run(), className: "p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-red-500", title: "Delete Column", children: [_jsx(Columns, { size: 14 }), "-"] }), _jsx("div", { className: "w-[1px] h-4 bg-gray-300 dark:bg-gray-600 mx-1" }), _jsxs("button", { type: "button", onClick: () => editor.chain().focus().addRowBefore().run(), className: "p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200", title: "Add Row Before", children: [_jsx(Rows, { size: 14, className: "rotate-180" }), "+"] }), _jsxs("button", { type: "button", onClick: () => editor.chain().focus().addRowAfter().run(), className: "p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200", title: "Add Row After", children: [_jsx(Rows, { size: 14 }), "+"] }), _jsxs("button", { type: "button", onClick: () => editor.chain().focus().deleteRow().run(), className: "p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-red-500", title: "Delete Row", children: [_jsx(Rows, { size: 14 }), "-"] }), _jsx("div", { className: "w-[1px] h-4 bg-gray-300 dark:bg-gray-600 mx-1" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().deleteTable().run(), className: "p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-red-500", title: "Delete Table", children: _jsx(Trash2, { size: 16 }) })] }));
|
|
8
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Node, mergeAttributes } from '@tiptap/core';
|
|
2
|
+
export const Container = Node.create({
|
|
3
|
+
name: 'container',
|
|
4
|
+
group: 'block',
|
|
5
|
+
content: 'block+', // Can contain paragraphs, lists, etc.
|
|
6
|
+
defining: true, // Prevents node from being replaced when content is pasted
|
|
7
|
+
isolating: false, // Allow backspace to delete/merge
|
|
8
|
+
addAttributes() {
|
|
9
|
+
return {
|
|
10
|
+
backgroundColor: {
|
|
11
|
+
default: null,
|
|
12
|
+
parseHTML: element => element.style.backgroundColor,
|
|
13
|
+
renderHTML: attributes => {
|
|
14
|
+
if (!attributes.backgroundColor)
|
|
15
|
+
return {};
|
|
16
|
+
return {
|
|
17
|
+
style: `background-color: ${attributes.backgroundColor}`,
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
borderRadius: {
|
|
22
|
+
default: null,
|
|
23
|
+
parseHTML: element => element.style.borderRadius,
|
|
24
|
+
renderHTML: attributes => {
|
|
25
|
+
if (!attributes.borderRadius)
|
|
26
|
+
return {};
|
|
27
|
+
return {
|
|
28
|
+
style: `border-radius: ${attributes.borderRadius}`,
|
|
29
|
+
};
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
parseHTML() {
|
|
35
|
+
return [
|
|
36
|
+
{
|
|
37
|
+
tag: 'div[data-type="container"]',
|
|
38
|
+
},
|
|
39
|
+
];
|
|
40
|
+
},
|
|
41
|
+
renderHTML({ HTMLAttributes }) {
|
|
42
|
+
return [
|
|
43
|
+
'div',
|
|
44
|
+
mergeAttributes(HTMLAttributes, {
|
|
45
|
+
'data-type': 'container',
|
|
46
|
+
class: 'nm-editor-container',
|
|
47
|
+
}),
|
|
48
|
+
0,
|
|
49
|
+
];
|
|
50
|
+
},
|
|
51
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Node } from '@tiptap/core';
|
|
2
|
+
export const GridContainer = Node.create({
|
|
3
|
+
name: 'gridContainer',
|
|
4
|
+
group: 'block',
|
|
5
|
+
content: 'gridItem+', // Contains only grid items
|
|
6
|
+
defining: true,
|
|
7
|
+
isolating: true,
|
|
8
|
+
parseHTML() {
|
|
9
|
+
return [
|
|
10
|
+
{ tag: 'div[data-type="grid-container"]', priority: 51 },
|
|
11
|
+
{ tag: 'div.nm-editor-grid', priority: 51 },
|
|
12
|
+
];
|
|
13
|
+
},
|
|
14
|
+
addAttributes() {
|
|
15
|
+
return {
|
|
16
|
+
backgroundColor: {
|
|
17
|
+
default: null,
|
|
18
|
+
parseHTML: element => element.style.getPropertyValue('--nm-grid-card-bg'),
|
|
19
|
+
renderHTML: attributes => {
|
|
20
|
+
if (!attributes.backgroundColor)
|
|
21
|
+
return {};
|
|
22
|
+
return {
|
|
23
|
+
style: `--nm-grid-card-bg: ${attributes.backgroundColor}`,
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
borderRadius: {
|
|
28
|
+
default: null,
|
|
29
|
+
parseHTML: element => element.style.getPropertyValue('--nm-grid-radius')?.replace(/["']/g, ''), // parse from var if possible, or just ignore since it's logical
|
|
30
|
+
renderHTML: attributes => {
|
|
31
|
+
if (!attributes.borderRadius)
|
|
32
|
+
return {};
|
|
33
|
+
return {
|
|
34
|
+
style: `--nm-grid-radius: ${attributes.borderRadius}`,
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
},
|
|
40
|
+
renderHTML({ HTMLAttributes }) {
|
|
41
|
+
return [
|
|
42
|
+
'div',
|
|
43
|
+
{
|
|
44
|
+
'data-type': 'grid-container',
|
|
45
|
+
...HTMLAttributes,
|
|
46
|
+
class: 'nm-editor-grid',
|
|
47
|
+
},
|
|
48
|
+
0,
|
|
49
|
+
];
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
export const GridItem = Node.create({
|
|
53
|
+
name: 'gridItem',
|
|
54
|
+
group: 'block',
|
|
55
|
+
content: 'block+', // Allow blocks (paragraphs) prevents flattening
|
|
56
|
+
defining: true,
|
|
57
|
+
isolating: true,
|
|
58
|
+
addAttributes() {
|
|
59
|
+
return {
|
|
60
|
+
href: {
|
|
61
|
+
default: null,
|
|
62
|
+
parseHTML: element => element.getAttribute('href'),
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
parseHTML() {
|
|
67
|
+
return [
|
|
68
|
+
{ tag: 'div[data-type="grid-item"]', priority: 60 },
|
|
69
|
+
{ tag: 'div.nm-editor-grid-card', priority: 60 },
|
|
70
|
+
{ tag: 'a[data-type="grid-item"]', priority: 60 },
|
|
71
|
+
{ tag: 'a.nm-editor-grid-card', priority: 60 },
|
|
72
|
+
];
|
|
73
|
+
},
|
|
74
|
+
renderHTML({ HTMLAttributes }) {
|
|
75
|
+
const { href, class: _c, className: _cn, ...attributesWithoutHref } = HTMLAttributes;
|
|
76
|
+
const hasHref = !!href;
|
|
77
|
+
const Tag = hasHref ? 'a' : 'div';
|
|
78
|
+
return [
|
|
79
|
+
Tag,
|
|
80
|
+
{
|
|
81
|
+
'data-type': 'grid-item',
|
|
82
|
+
...attributesWithoutHref,
|
|
83
|
+
...(hasHref ? { href } : {}),
|
|
84
|
+
class: 'nm-editor-grid-card',
|
|
85
|
+
},
|
|
86
|
+
0,
|
|
87
|
+
];
|
|
88
|
+
},
|
|
89
|
+
});
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { Node, mergeAttributes } from '@tiptap/core';
|
|
2
|
+
export const LayoutRow = Node.create({
|
|
3
|
+
name: 'layoutRow',
|
|
4
|
+
group: 'block',
|
|
5
|
+
content: 'layoutColumn+', // Must contain one or more columns
|
|
6
|
+
defining: true,
|
|
7
|
+
isolating: true,
|
|
8
|
+
addAttributes() {
|
|
9
|
+
return {
|
|
10
|
+
cols: {
|
|
11
|
+
default: 2,
|
|
12
|
+
parseHTML: element => element.getAttribute('data-cols'),
|
|
13
|
+
renderHTML: attributes => ({
|
|
14
|
+
'data-cols': attributes.cols,
|
|
15
|
+
}),
|
|
16
|
+
},
|
|
17
|
+
backgroundColor: {
|
|
18
|
+
default: null,
|
|
19
|
+
parseHTML: element => element.style.backgroundColor,
|
|
20
|
+
renderHTML: attributes => {
|
|
21
|
+
if (!attributes.backgroundColor)
|
|
22
|
+
return {};
|
|
23
|
+
return {
|
|
24
|
+
style: `background-color: ${attributes.backgroundColor}`,
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
borderRadius: {
|
|
29
|
+
default: null,
|
|
30
|
+
parseHTML: element => element.style.borderRadius,
|
|
31
|
+
renderHTML: attributes => {
|
|
32
|
+
if (!attributes.borderRadius)
|
|
33
|
+
return {};
|
|
34
|
+
return {
|
|
35
|
+
style: `border-radius: ${attributes.borderRadius}`,
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
parseHTML() {
|
|
42
|
+
return [
|
|
43
|
+
{
|
|
44
|
+
tag: 'div[data-type="layout-row"]',
|
|
45
|
+
},
|
|
46
|
+
];
|
|
47
|
+
},
|
|
48
|
+
renderHTML({ HTMLAttributes }) {
|
|
49
|
+
const cols = HTMLAttributes.cols || 2;
|
|
50
|
+
let gridClass = 'nm-editor-row-2';
|
|
51
|
+
if (cols == 3) {
|
|
52
|
+
gridClass = 'nm-editor-row-3';
|
|
53
|
+
}
|
|
54
|
+
else if (cols == 1) {
|
|
55
|
+
gridClass = 'nm-editor-row-1';
|
|
56
|
+
}
|
|
57
|
+
return [
|
|
58
|
+
'div',
|
|
59
|
+
mergeAttributes(HTMLAttributes, {
|
|
60
|
+
'data-type': 'layout-row',
|
|
61
|
+
class: gridClass,
|
|
62
|
+
}),
|
|
63
|
+
0,
|
|
64
|
+
];
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
export const LayoutColumn = Node.create({
|
|
68
|
+
name: 'layoutColumn',
|
|
69
|
+
group: 'block',
|
|
70
|
+
content: 'block+', // Can contain paragraphs, lists, images, etc.
|
|
71
|
+
defining: true,
|
|
72
|
+
isolating: true,
|
|
73
|
+
addAttributes() {
|
|
74
|
+
return {
|
|
75
|
+
backgroundColor: {
|
|
76
|
+
default: null,
|
|
77
|
+
parseHTML: element => element.style.backgroundColor,
|
|
78
|
+
renderHTML: attributes => {
|
|
79
|
+
if (!attributes.backgroundColor)
|
|
80
|
+
return {};
|
|
81
|
+
return {
|
|
82
|
+
style: `background-color: ${attributes.backgroundColor}`,
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
borderRadius: {
|
|
87
|
+
default: null,
|
|
88
|
+
parseHTML: element => element.style.borderRadius,
|
|
89
|
+
renderHTML: attributes => {
|
|
90
|
+
if (!attributes.borderRadius)
|
|
91
|
+
return {};
|
|
92
|
+
return {
|
|
93
|
+
style: `border-radius: ${attributes.borderRadius}`,
|
|
94
|
+
};
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
parseHTML() {
|
|
100
|
+
return [
|
|
101
|
+
{
|
|
102
|
+
tag: 'div[data-type="layout-column"]',
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
},
|
|
106
|
+
renderHTML({ HTMLAttributes }) {
|
|
107
|
+
return [
|
|
108
|
+
'div',
|
|
109
|
+
mergeAttributes(HTMLAttributes, {
|
|
110
|
+
'data-type': 'layout-column',
|
|
111
|
+
class: 'nm-editor-col',
|
|
112
|
+
}),
|
|
113
|
+
0,
|
|
114
|
+
];
|
|
115
|
+
},
|
|
116
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const ResizableImage: import("@tiptap/core").Node<import("@tiptap/extension-image").ImageOptions, any>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import Image from '@tiptap/extension-image';
|
|
2
|
+
import { ReactNodeViewRenderer } from '@tiptap/react';
|
|
3
|
+
import { ImageComponent } from '../components/ImageComponent';
|
|
4
|
+
import { mergeAttributes } from '@tiptap/core';
|
|
5
|
+
export const ResizableImage = Image.extend({
|
|
6
|
+
name: 'image',
|
|
7
|
+
group: 'block',
|
|
8
|
+
inline: false,
|
|
9
|
+
draggable: true,
|
|
10
|
+
addAttributes() {
|
|
11
|
+
return {
|
|
12
|
+
...this.parent?.(),
|
|
13
|
+
width: {
|
|
14
|
+
default: null,
|
|
15
|
+
renderHTML: (attributes) => {
|
|
16
|
+
if (!attributes.width)
|
|
17
|
+
return {};
|
|
18
|
+
return { width: attributes.width };
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
height: {
|
|
22
|
+
default: null,
|
|
23
|
+
renderHTML: (attributes) => {
|
|
24
|
+
if (!attributes.height)
|
|
25
|
+
return {};
|
|
26
|
+
return { height: attributes.height };
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
src: {
|
|
30
|
+
default: '',
|
|
31
|
+
},
|
|
32
|
+
alt: {
|
|
33
|
+
default: '',
|
|
34
|
+
},
|
|
35
|
+
objectFit: {
|
|
36
|
+
default: 'contain', // cover, contain, fill
|
|
37
|
+
renderHTML: (attributes) => {
|
|
38
|
+
if (!attributes.objectFit)
|
|
39
|
+
return {};
|
|
40
|
+
return { style: `object-fit: ${attributes.objectFit}` };
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
addNodeView() {
|
|
46
|
+
return ReactNodeViewRenderer(ImageComponent);
|
|
47
|
+
},
|
|
48
|
+
// Custom renderHTML to ensure attributes are correct in output
|
|
49
|
+
renderHTML({ HTMLAttributes }) {
|
|
50
|
+
return ['img', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes)];
|
|
51
|
+
},
|
|
52
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core';
|
|
2
|
+
import { SchemaDef } from '../../../lib/types';
|
|
3
|
+
export declare const SlashCommand: Extension<any, any>;
|
|
4
|
+
export declare const getSuggestionOptions: (items: SchemaDef[], onSelect?: (item: SchemaDef) => void) => {
|
|
5
|
+
items: ({ query }: {
|
|
6
|
+
query: string;
|
|
7
|
+
}) => SchemaDef[];
|
|
8
|
+
command: ({ editor, range }: any) => void;
|
|
9
|
+
render: () => {
|
|
10
|
+
onStart: (props: any) => void;
|
|
11
|
+
onUpdate(props: any): void;
|
|
12
|
+
onKeyDown(props: any): any;
|
|
13
|
+
onExit(): void;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Extension } from '@tiptap/core';
|
|
2
|
+
import Suggestion from '@tiptap/suggestion';
|
|
3
|
+
import { ReactRenderer } from '@tiptap/react';
|
|
4
|
+
import { CommandList } from '../components/CommandList';
|
|
5
|
+
import { List, LayoutTemplate, LayoutPanelLeft } from 'lucide-react'; // Assuming these icons are available
|
|
6
|
+
export const SlashCommand = Extension.create({
|
|
7
|
+
name: 'slashCommand',
|
|
8
|
+
addOptions() {
|
|
9
|
+
return {
|
|
10
|
+
suggestion: {
|
|
11
|
+
char: '/',
|
|
12
|
+
command: ({ editor, range }) => {
|
|
13
|
+
// Just delete the range (the "/") to clean up
|
|
14
|
+
editor.chain().focus().deleteRange(range).run();
|
|
15
|
+
},
|
|
16
|
+
items: ({ query }) => {
|
|
17
|
+
const allCommands = [
|
|
18
|
+
{
|
|
19
|
+
title: 'Bullet List',
|
|
20
|
+
description: 'Create a simple bullet list.',
|
|
21
|
+
icon: List,
|
|
22
|
+
command: ({ editor, range }) => {
|
|
23
|
+
editor.chain().focus().deleteRange(range).toggleBulletList().run();
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
title: '2 Columns',
|
|
28
|
+
description: 'Insert 2 responsive columns',
|
|
29
|
+
icon: LayoutTemplate,
|
|
30
|
+
command: ({ editor, range }) => {
|
|
31
|
+
editor
|
|
32
|
+
.chain()
|
|
33
|
+
.focus()
|
|
34
|
+
.deleteRange(range)
|
|
35
|
+
.insertContent('<p></p>') // Ensure separation
|
|
36
|
+
.insertContent('<div data-type="layout-row" data-cols="2"><div data-type="layout-column"><p></p></div><div data-type="layout-column"><p></p></div></div>')
|
|
37
|
+
.run();
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
title: '3 Columns',
|
|
42
|
+
description: 'Insert 3 responsive columns',
|
|
43
|
+
icon: LayoutPanelLeft,
|
|
44
|
+
command: ({ editor, range }) => {
|
|
45
|
+
editor
|
|
46
|
+
.chain()
|
|
47
|
+
.focus()
|
|
48
|
+
.deleteRange(range)
|
|
49
|
+
.insertContent('<p></p>') // Ensure separation
|
|
50
|
+
.insertContent('<div data-type="layout-row" data-cols="3"><div data-type="layout-column"><p></p></div><div data-type="layout-column"><p></p></div><div data-type="layout-column"><p></p></div></div>')
|
|
51
|
+
.run();
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
// Add other static commands here if any
|
|
55
|
+
];
|
|
56
|
+
// Filter commands based on query
|
|
57
|
+
return allCommands
|
|
58
|
+
.filter((item) => item.title.toLowerCase().startsWith(query.toLowerCase()))
|
|
59
|
+
.slice(0, 10);
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
addProseMirrorPlugins() {
|
|
65
|
+
return [
|
|
66
|
+
Suggestion({
|
|
67
|
+
editor: this.editor,
|
|
68
|
+
...this.options.suggestion,
|
|
69
|
+
}),
|
|
70
|
+
];
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
export const getSuggestionOptions = (items, onSelect) => ({
|
|
74
|
+
items: ({ query }) => {
|
|
75
|
+
return items
|
|
76
|
+
.filter((item) => item.modelName.toLowerCase().startsWith(query.toLowerCase()))
|
|
77
|
+
.slice(0, 10);
|
|
78
|
+
},
|
|
79
|
+
command: ({ editor, range }) => {
|
|
80
|
+
// For schema-only suggestions, we just delete the range.
|
|
81
|
+
// The actual selection logic (onSelect) is handled in the render function's command prop.
|
|
82
|
+
editor.chain().focus().deleteRange(range).run();
|
|
83
|
+
},
|
|
84
|
+
render: () => {
|
|
85
|
+
let component;
|
|
86
|
+
let popup = null;
|
|
87
|
+
let container = null;
|
|
88
|
+
return {
|
|
89
|
+
onStart: (props) => {
|
|
90
|
+
component = new ReactRenderer(CommandList, {
|
|
91
|
+
props: {
|
|
92
|
+
...props,
|
|
93
|
+
// Fix 1: Explicitly pass the command prop expected by CommandList
|
|
94
|
+
command: (item) => {
|
|
95
|
+
// If external callback provided, use it
|
|
96
|
+
if (onSelect) {
|
|
97
|
+
onSelect(item);
|
|
98
|
+
}
|
|
99
|
+
// CRITICAL: Call the original command to close the popup and delete the "/"
|
|
100
|
+
props.command(item);
|
|
101
|
+
},
|
|
102
|
+
items: props.items,
|
|
103
|
+
},
|
|
104
|
+
editor: props.editor,
|
|
105
|
+
});
|
|
106
|
+
// Create container for popup
|
|
107
|
+
container = document.createElement('div');
|
|
108
|
+
container.style.position = 'absolute';
|
|
109
|
+
container.style.zIndex = '9999';
|
|
110
|
+
container.style.display = 'none'; // Hidden until positioned
|
|
111
|
+
document.body.appendChild(container);
|
|
112
|
+
// Mount component
|
|
113
|
+
// @ts-ignore
|
|
114
|
+
container.appendChild(component.element);
|
|
115
|
+
// Position it
|
|
116
|
+
const coords = props.clientRect?.();
|
|
117
|
+
if (coords && container) {
|
|
118
|
+
container.style.display = 'block';
|
|
119
|
+
container.style.left = `${coords.left}px`;
|
|
120
|
+
container.style.top = `${coords.bottom + 10}px`;
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
onUpdate(props) {
|
|
124
|
+
component.updateProps({
|
|
125
|
+
...props,
|
|
126
|
+
// Fix 2: Ensure command prop is preserved during updates
|
|
127
|
+
command: (item) => {
|
|
128
|
+
if (onSelect) {
|
|
129
|
+
onSelect(item);
|
|
130
|
+
}
|
|
131
|
+
props.command(item);
|
|
132
|
+
},
|
|
133
|
+
items: props.items,
|
|
134
|
+
});
|
|
135
|
+
const coords = props.clientRect?.();
|
|
136
|
+
if (coords && container) {
|
|
137
|
+
container.style.left = `${coords.left}px`;
|
|
138
|
+
container.style.top = `${coords.bottom + 10}px`;
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
onKeyDown(props) {
|
|
142
|
+
if (props.event.key === 'Escape') {
|
|
143
|
+
if (container) {
|
|
144
|
+
container.remove();
|
|
145
|
+
container = null;
|
|
146
|
+
}
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
// @ts-ignore
|
|
150
|
+
return component.ref?.onKeyDown(props);
|
|
151
|
+
},
|
|
152
|
+
onExit() {
|
|
153
|
+
if (container) {
|
|
154
|
+
container.remove();
|
|
155
|
+
container = null;
|
|
156
|
+
}
|
|
157
|
+
component.destroy();
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
},
|
|
161
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function uploadFile(file: File): Promise<string>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Utility to upload files reusing existing API logic
|
|
2
|
+
const DEFAULT_ENDPOINT = ((process.env.NEXT_PUBLIC_NEXTMIN_API_URL || '') + '/files').replace(/\/+$/, '') || '/files';
|
|
3
|
+
const defaultAuthHeaders = () => {
|
|
4
|
+
if (typeof window === 'undefined')
|
|
5
|
+
return {};
|
|
6
|
+
let token;
|
|
7
|
+
let apiKey;
|
|
8
|
+
try {
|
|
9
|
+
const raw = localStorage.getItem('nextmin.user');
|
|
10
|
+
if (raw) {
|
|
11
|
+
const u = JSON.parse(raw);
|
|
12
|
+
token = u?.token ?? u?.data?.token;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
catch { }
|
|
16
|
+
token = token ?? localStorage.getItem('nextmin.token') ?? undefined;
|
|
17
|
+
apiKey =
|
|
18
|
+
localStorage.getItem('nextmin.apiKey') ??
|
|
19
|
+
process.env.NEXT_PUBLIC_NEXTMIN_API_KEY;
|
|
20
|
+
const h = {};
|
|
21
|
+
if (token)
|
|
22
|
+
h.Authorization = `Bearer ${token}`;
|
|
23
|
+
if (apiKey)
|
|
24
|
+
h['x-api-key'] = apiKey;
|
|
25
|
+
return h;
|
|
26
|
+
};
|
|
27
|
+
export async function uploadFile(file) {
|
|
28
|
+
const headers = defaultAuthHeaders();
|
|
29
|
+
const endpoint = DEFAULT_ENDPOINT;
|
|
30
|
+
// XHR for progress support if needed, but fetch is simpler for now
|
|
31
|
+
const formData = new FormData();
|
|
32
|
+
formData.append('file', file);
|
|
33
|
+
const response = await fetch(endpoint, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: {
|
|
36
|
+
...headers
|
|
37
|
+
// Content-Type is set automatically by fetch with FormData
|
|
38
|
+
},
|
|
39
|
+
body: formData
|
|
40
|
+
});
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
throw new Error('Upload failed');
|
|
43
|
+
}
|
|
44
|
+
const json = await response.json();
|
|
45
|
+
if (!json.success || !json.data || !json.data.length) {
|
|
46
|
+
throw new Error(json.message || 'Upload failed');
|
|
47
|
+
}
|
|
48
|
+
return json.data[0].url;
|
|
49
|
+
}
|