@airoom/nextmin-react 1.4.0 → 1.4.2
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 +17 -2
- package/dist/components/SchemaSuggestionList.d.ts +7 -0
- package/dist/components/SchemaSuggestionList.js +43 -0
- package/dist/components/editor/TiptapEditor.d.ts +11 -0
- package/dist/components/editor/TiptapEditor.js +330 -0
- package/dist/components/editor/Toolbar.d.ts +7 -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/DistrictGridModal.d.ts +12 -0
- package/dist/components/editor/components/DistrictGridModal.js +67 -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 +15 -0
- package/dist/components/editor/components/SchemaInsertionModal.js +91 -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,67 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button, } from '@heroui/react';
|
|
5
|
+
import { RefMultiSelect } from '../../RefMultiSelect';
|
|
6
|
+
import { RefSingleSelect } from '../../RefSingleSelect';
|
|
7
|
+
import { cn } from '../../../lib/utils';
|
|
8
|
+
import { api } from '../../../lib/api';
|
|
9
|
+
export const DistrictGridModal = ({ isOpen, onClose, onInsert, currentSpeciality, }) => {
|
|
10
|
+
const [baseType, setBaseType] = useState('doctors');
|
|
11
|
+
const [selectedDistricts, setSelectedDistricts] = useState([]);
|
|
12
|
+
const [selectedSpecialityId, setSelectedSpecialityId] = useState('');
|
|
13
|
+
const [specialitySlug, setSpecialitySlug] = useState('');
|
|
14
|
+
// Fetch speciality data when ID changes
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (!selectedSpecialityId) {
|
|
17
|
+
setSpecialitySlug('');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const fetchSpeciality = async () => {
|
|
21
|
+
try {
|
|
22
|
+
const res = await api.get('Specialities', selectedSpecialityId);
|
|
23
|
+
const speciality = res.data || res;
|
|
24
|
+
setSpecialitySlug(speciality?.slug || '');
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
console.error('Failed to fetch speciality:', err);
|
|
28
|
+
setSpecialitySlug('');
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
fetchSpeciality();
|
|
32
|
+
}, [selectedSpecialityId]);
|
|
33
|
+
const handleInsert = () => {
|
|
34
|
+
onInsert({
|
|
35
|
+
districts: selectedDistricts,
|
|
36
|
+
baseType,
|
|
37
|
+
specialitySlug: specialitySlug
|
|
38
|
+
});
|
|
39
|
+
onClose();
|
|
40
|
+
// Reset state after a short delay
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
setBaseType('doctors');
|
|
43
|
+
setSelectedDistricts([]);
|
|
44
|
+
setSelectedSpecialityId('');
|
|
45
|
+
setSpecialitySlug('');
|
|
46
|
+
}, 200);
|
|
47
|
+
};
|
|
48
|
+
const baseFrontendUrl = process.env.NEXT_PUBLIC_FRONTEND_URL || '';
|
|
49
|
+
const cleanBase = baseFrontendUrl.replace(/\/$/, '');
|
|
50
|
+
// Generate preview URL
|
|
51
|
+
const getPreviewUrl = (districtSlug) => {
|
|
52
|
+
const parts = [cleanBase, baseType, districtSlug];
|
|
53
|
+
if (specialitySlug) {
|
|
54
|
+
parts.push(specialitySlug);
|
|
55
|
+
}
|
|
56
|
+
return parts.join('/');
|
|
57
|
+
};
|
|
58
|
+
return (_jsx(Modal, { isOpen: isOpen, onClose: onClose, size: "2xl", classNames: {
|
|
59
|
+
base: "bg-white dark:bg-zinc-950 border border-gray-200 dark:border-gray-800",
|
|
60
|
+
header: "border-b border-gray-200 dark:border-gray-800",
|
|
61
|
+
footer: "border-t border-gray-200 dark:border-gray-800",
|
|
62
|
+
}, children: _jsx(ModalContent, { children: _jsxs(_Fragment, { children: [_jsx(ModalHeader, { children: "Insert District Grid" }), _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: "Base Type" }), _jsxs("div", { className: "flex gap-4", children: [_jsx("button", { onClick: () => setBaseType('doctors'), className: cn("px-4 py-2 rounded-lg border text-sm font-medium transition-colors", baseType === 'doctors'
|
|
63
|
+
? "bg-primary-50 border-primary-500 text-primary-700 dark:bg-primary-900/20 dark:text-primary-400"
|
|
64
|
+
: "bg-transparent border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-900"), children: "Doctors" }), _jsx("button", { onClick: () => setBaseType('hospitals'), className: cn("px-4 py-2 rounded-lg border text-sm font-medium transition-colors", baseType === 'hospitals'
|
|
65
|
+
? "bg-primary-50 border-primary-500 text-primary-700 dark:bg-primary-900/20 dark:text-primary-400"
|
|
66
|
+
: "bg-transparent border-gray-200 dark:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-900"), children: "Hospitals" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("label", { className: "text-sm font-medium", children: "Select Districts" }), _jsx(RefMultiSelect, { name: "districts", label: "Search & Select Districts", refModel: "Districts", value: selectedDistricts, onChange: (ids) => setSelectedDistricts(ids), pageSize: 64 }), _jsx("p", { className: "text-xs text-gray-400", children: "Select one or more districts to create grid items." })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("label", { className: "text-sm font-medium", children: "Select Speciality (Optional)" }), _jsx(RefSingleSelect, { name: "speciality", label: "Search & Select Speciality", refModel: "Specialities", showKey: "name", value: selectedSpecialityId, onChange: (id) => setSelectedSpecialityId(id || ''), pageSize: 50 }), _jsx("p", { className: "text-xs text-gray-400", children: "Leave empty to create URLs without speciality." }), specialitySlug && (_jsx("div", { className: "mt-1 px-2 py-1 bg-primary-50 dark:bg-primary-900/20 rounded border border-primary-200 dark:border-primary-800", children: _jsxs("p", { className: "text-xs text-primary-700 dark:text-primary-400", children: ["Slug: ", _jsx("code", { className: "font-mono", children: specialitySlug })] }) }))] }), selectedDistricts.length > 0 && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx("label", { className: "text-sm font-medium", children: "URL Preview" }), _jsxs("div", { className: "px-3 py-2 bg-gray-50 dark:bg-gray-900 rounded-lg border border-gray-200 dark:border-gray-800 max-h-32 overflow-y-auto", children: [_jsx("p", { className: "text-xs text-gray-500 mb-2", children: "Example URL pattern (actual district slugs will be used):" }), _jsx("code", { className: "text-xs text-gray-700 dark:text-gray-300", children: getPreviewUrl('[district-slug]') })] })] }))] }), _jsxs(ModalFooter, { children: [_jsx(Button, { variant: "flat", onPress: onClose, children: "Cancel" }), _jsx(Button, { color: "primary", onPress: handleInsert, isDisabled: selectedDistricts.length === 0, children: "Insert Grid" })] })] }) }) }));
|
|
67
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { BubbleMenu } from '@tiptap/react/menus';
|
|
3
|
+
import { Minimize, AlignLeft, AlignCenter, AlignRight, StretchHorizontal } from 'lucide-react';
|
|
4
|
+
import { cn } from '../../../lib/utils';
|
|
5
|
+
export const ImageBubbleMenu = ({ editor }) => {
|
|
6
|
+
if (!editor)
|
|
7
|
+
return null;
|
|
8
|
+
return (_jsxs(BubbleMenu, { editor: editor, pluginKey: "imageBubbleMenu", shouldShow: ({ editor }) => editor.isActive('image'),
|
|
9
|
+
// @ts-ignore
|
|
10
|
+
tippyOptions: {
|
|
11
|
+
duration: 100,
|
|
12
|
+
zIndex: 99, // Ensure it sits above other elements
|
|
13
|
+
maxWidth: 'none',
|
|
14
|
+
}, 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 z-50", children: [_jsx("button", { type: "button", onClick: () => editor.chain().focus().updateAttributes('image', { width: '100%', height: null }).run(), className: cn("p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200", editor.getAttributes('image').width === '100%' && "bg-gray-200 dark:bg-gray-600"), title: "Full Width", children: _jsx(StretchHorizontal, { size: 16 }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().updateAttributes('image', { width: null, height: null }).run(), className: "p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200", title: "Original Size", children: _jsx(Minimize, { size: 16 }) }), _jsx("div", { className: "w-[1px] h-4 bg-gray-300 dark:bg-gray-600 mx-1" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().setTextAlign('left').run(), className: cn("p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200", editor.isActive({ textAlign: 'left' }) && "bg-gray-200 dark:bg-gray-600"), title: "Align Left", children: _jsx(AlignLeft, { size: 16 }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().setTextAlign('center').run(), className: cn("p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200", editor.isActive({ textAlign: 'center' }) && "bg-gray-200 dark:bg-gray-600"), title: "Align Center", children: _jsx(AlignCenter, { size: 16 }) }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().setTextAlign('right').run(), className: cn("p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200", editor.isActive({ textAlign: 'right' }) && "bg-gray-200 dark:bg-gray-600"), title: "Align Right", children: _jsx(AlignRight, { size: 16 }) }), _jsx("div", { className: "w-[1px] h-4 bg-gray-300 dark:bg-gray-600 mx-1" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().updateAttributes('image', { objectFit: 'contain' }).run(), className: cn("p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 text-xs font-medium", editor.getAttributes('image').objectFit === 'contain' && "bg-gray-200 dark:bg-gray-600"), title: "Contain", children: "Fit" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().updateAttributes('image', { objectFit: 'cover' }).run(), className: cn("p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 text-xs font-medium", editor.getAttributes('image').objectFit === 'cover' && "bg-gray-200 dark:bg-gray-600"), title: "Cover", children: "Cover" }), _jsx("button", { type: "button", onClick: () => editor.chain().focus().updateAttributes('image', { objectFit: 'fill' }).run(), className: cn("p-1.5 rounded hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-200 text-xs font-medium", editor.getAttributes('image').objectFit === 'fill' && "bg-gray-200 dark:bg-gray-600"), title: "Fill", children: "Fill" })] }));
|
|
15
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { NodeViewWrapper } from '@tiptap/react';
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
import { ResizableBox } from 'react-resizable';
|
|
5
|
+
import clsx from 'clsx';
|
|
6
|
+
export const ImageComponent = (props) => {
|
|
7
|
+
const { node, updateAttributes, selected } = props;
|
|
8
|
+
const { src, alt, width, height, textAlign, objectFit } = node.attrs;
|
|
9
|
+
// Determine if we are in "Full Width" mode
|
|
10
|
+
const isFullWidth = width === '100%';
|
|
11
|
+
const [w, setW] = useState(width || 'auto');
|
|
12
|
+
const [h, setH] = useState(height || 'auto');
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
setW(width || 'auto');
|
|
15
|
+
setH(height || 'auto');
|
|
16
|
+
}, [width, height]);
|
|
17
|
+
const onResize = (e, { size }) => {
|
|
18
|
+
setW(size.width);
|
|
19
|
+
setH(size.height);
|
|
20
|
+
};
|
|
21
|
+
const onResizeStop = (e, { size }) => {
|
|
22
|
+
updateAttributes({
|
|
23
|
+
width: size.width,
|
|
24
|
+
height: size.height,
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
const getSimulatedWidth = () => (typeof w === 'number' ? w : 300);
|
|
28
|
+
const getSimulatedHeight = () => (typeof h === 'number' ? h : 300);
|
|
29
|
+
const alignmentClass = textAlign === 'center'
|
|
30
|
+
? 'image-align-center'
|
|
31
|
+
: textAlign === 'right'
|
|
32
|
+
? 'image-align-right'
|
|
33
|
+
: textAlign === 'left'
|
|
34
|
+
? 'image-align-left'
|
|
35
|
+
: 'image-align-default';
|
|
36
|
+
const renderImage = () => (_jsx("img", { src: src, alt: alt, style: {
|
|
37
|
+
width: isFullWidth ? '100%' : '100%',
|
|
38
|
+
height: isFullWidth ? 'auto' : '100%',
|
|
39
|
+
display: 'block',
|
|
40
|
+
objectFit: objectFit || 'contain',
|
|
41
|
+
}, className: clsx("nm-editor-img", isFullWidth && "nm-editor-w-full"), draggable: false }));
|
|
42
|
+
return (_jsx(NodeViewWrapper, { className: clsx('image-component-wrapper nm-editor-img-wrapper', alignmentClass, isFullWidth && 'nm-editor-w-full'), children: _jsx("div", { className: clsx('nm-editor-img-container group', selected && !isFullWidth && 'nm-editor-ring-selected', isFullWidth && 'nm-editor-w-full'), children: isFullWidth ? (_jsx("div", { className: clsx("nm-editor-w-full", selected && "nm-editor-ring-selected"), children: renderImage() })) : (_jsx(ResizableBox, { width: getSimulatedWidth(), height: getSimulatedHeight(), onResize: onResize, onResizeStop: onResizeStop, lockAspectRatio: true, draggableOpts: { enableUserSelectHack: false }, resizeHandles: selected ? ['sw', 'se', 'nw', 'ne'] : [], handle: (h, ref) => (_jsx("div", { className: `image-resizer image-resizer-${h}`,
|
|
43
|
+
// @ts-ignore
|
|
44
|
+
ref: ref })), children: renderImage() })) }) }));
|
|
45
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { SchemaDef } from '../../../lib/types';
|
|
2
|
+
interface SchemaInsertionModalProps {
|
|
3
|
+
isOpen: boolean;
|
|
4
|
+
onClose: () => void;
|
|
5
|
+
onInsert: (config: {
|
|
6
|
+
viewType: 'grid' | 'list';
|
|
7
|
+
template: string;
|
|
8
|
+
ids: string[];
|
|
9
|
+
urlPattern: string;
|
|
10
|
+
}) => void;
|
|
11
|
+
schema: SchemaDef | null;
|
|
12
|
+
availableSchemas: SchemaDef[];
|
|
13
|
+
}
|
|
14
|
+
export declare const SchemaInsertionModal: ({ isOpen, onClose, onInsert, schema, availableSchemas, }: SchemaInsertionModalProps) => import("react/jsx-runtime").JSX.Element | null;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import React, { 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
|
+
// Helper to get nested keys with recursion limit
|
|
11
|
+
const getAllKeys = (attributes, allSchemas, prefix = '', depth = 0) => {
|
|
12
|
+
if (depth > 2)
|
|
13
|
+
return []; // Prevent infinite recursion
|
|
14
|
+
let keys = [];
|
|
15
|
+
Object.keys(attributes).forEach(key => {
|
|
16
|
+
const attr = attributes[key];
|
|
17
|
+
const currentKey = prefix ? `${prefix}.${key}` : key;
|
|
18
|
+
keys.push(currentKey);
|
|
19
|
+
// Check for reference
|
|
20
|
+
let refModelName;
|
|
21
|
+
if (attr.ref) {
|
|
22
|
+
refModelName = attr.ref;
|
|
23
|
+
}
|
|
24
|
+
else if (Array.isArray(attr) && attr[0]?.ref) {
|
|
25
|
+
refModelName = attr[0].ref;
|
|
26
|
+
}
|
|
27
|
+
if (refModelName) {
|
|
28
|
+
const refSchema = allSchemas.find(s => s.modelName === refModelName);
|
|
29
|
+
if (refSchema) {
|
|
30
|
+
const subKeys = getAllKeys(refSchema.attributes, allSchemas, currentKey, depth + 1);
|
|
31
|
+
keys = [...keys, ...subKeys];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else if (typeof attr === 'object' && !attr.type && !Array.isArray(attr)) {
|
|
35
|
+
// Plain nested object (not an attribute definition itself, but a grouping)
|
|
36
|
+
// Check if it has type property? SchemaDef attributes are Record<string, Attribute>.
|
|
37
|
+
// Attribute can be AttributeBase or AttributeBase[].
|
|
38
|
+
// If it doesn't have 'type', it might be a nested object structure if supported.
|
|
39
|
+
// But based on types.ts, attributes is Record<string, Attribute>.
|
|
40
|
+
// So we strictly look for ref in AttributeBase.
|
|
41
|
+
// If keys are just grouping (e.g. meta: { title: ... }), then we recurse.
|
|
42
|
+
// But Mongoose schema defs usually have type.
|
|
43
|
+
// Let's assume structure is flat or standard Mongoose.
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
// Add common virtuals at appropriate levels if needed, or just top level
|
|
47
|
+
if (prefix === '') {
|
|
48
|
+
keys.push('id');
|
|
49
|
+
keys.push('createdAt');
|
|
50
|
+
keys.push('updatedAt');
|
|
51
|
+
}
|
|
52
|
+
return keys;
|
|
53
|
+
};
|
|
54
|
+
export const SchemaInsertionModal = ({ isOpen, onClose, onInsert, schema, availableSchemas, }) => {
|
|
55
|
+
const [viewType, setViewType] = useState('grid');
|
|
56
|
+
const [template, setTemplate] = useState('');
|
|
57
|
+
const [urlPattern, setUrlPattern] = useState('{slug}_{id}');
|
|
58
|
+
const [selectedIds, setSelectedIds] = useState([]);
|
|
59
|
+
const handleInsert = () => {
|
|
60
|
+
onInsert({ viewType, template, ids: selectedIds, urlPattern });
|
|
61
|
+
onClose();
|
|
62
|
+
setTimeout(() => {
|
|
63
|
+
setViewType('grid');
|
|
64
|
+
setTemplate('');
|
|
65
|
+
setUrlPattern('{slug}_{id}');
|
|
66
|
+
setSelectedIds([]);
|
|
67
|
+
}, 200);
|
|
68
|
+
};
|
|
69
|
+
const availableKeys = React.useMemo(() => {
|
|
70
|
+
if (!schema)
|
|
71
|
+
return [];
|
|
72
|
+
return getAllKeys(schema.attributes, availableSchemas);
|
|
73
|
+
}, [schema, availableSchemas]);
|
|
74
|
+
const addToTemplate = (key) => {
|
|
75
|
+
setTemplate(prev => `${prev} {${key}}`);
|
|
76
|
+
};
|
|
77
|
+
const addToUrlPattern = (key) => {
|
|
78
|
+
setUrlPattern(prev => `${prev}{${key}}`);
|
|
79
|
+
};
|
|
80
|
+
if (!schema)
|
|
81
|
+
return null;
|
|
82
|
+
return (_jsx(Modal, { isOpen: isOpen, onClose: onClose, size: "2xl", classNames: {
|
|
83
|
+
base: "bg-white dark:bg-zinc-950 border border-gray-200 dark:border-gray-800",
|
|
84
|
+
header: "border-b border-gray-200 dark:border-gray-800",
|
|
85
|
+
footer: "border-t border-gray-200 dark:border-gray-800",
|
|
86
|
+
}, 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'
|
|
87
|
+
? "bg-primary-50 border-primary-500 text-primary-700 dark:bg-primary-900/20 dark:text-primary-400"
|
|
88
|
+
: "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'
|
|
89
|
+
? "bg-primary-50 border-primary-500 text-primary-700 dark:bg-primary-900/20 dark:text-primary-400"
|
|
90
|
+
: "bg-transparent border-gray-200 dark:border-gray-800 hover:bg-gray-50"), children: "List (Rows)" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_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("div", { className: "flex flex-wrap gap-2 mt-1", children: availableKeys.map(key => (_jsx("button", { onClick: () => addToTemplate(key), className: "px-2 py-1 text-xs bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors", children: key }, key))) })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_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" }), _jsx("div", { className: "flex flex-wrap gap-2 mt-1", children: availableKeys.map(key => (_jsx("button", { onClick: () => addToUrlPattern(key), className: "px-2 py-1 text-xs bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors", children: key }, key))) })] })] }), _jsxs(ModalFooter, { children: [_jsx(Button, { variant: "flat", onPress: onClose, children: "Cancel" }), _jsx(Button, { color: "primary", onPress: handleInsert, children: "Insert" })] })] }) }) }));
|
|
91
|
+
};
|
|
@@ -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
|
+
};
|