@airoom/nextmin-react 1.3.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.
Files changed (40) hide show
  1. package/dist/components/SchemaForm.js +15 -2
  2. package/dist/components/SchemaSuggestionList.d.ts +7 -0
  3. package/dist/components/SchemaSuggestionList.js +43 -0
  4. package/dist/components/editor/TiptapEditor.d.ts +10 -0
  5. package/dist/components/editor/TiptapEditor.js +228 -0
  6. package/dist/components/editor/Toolbar.d.ts +6 -0
  7. package/dist/components/editor/Toolbar.js +99 -0
  8. package/dist/components/editor/components/CommandList.d.ts +7 -0
  9. package/dist/components/editor/components/CommandList.js +47 -0
  10. package/dist/components/editor/components/ImageBubbleMenu.d.ts +6 -0
  11. package/dist/components/editor/components/ImageBubbleMenu.js +15 -0
  12. package/dist/components/editor/components/ImageComponent.d.ts +3 -0
  13. package/dist/components/editor/components/ImageComponent.js +45 -0
  14. package/dist/components/editor/components/SchemaInsertionModal.d.ts +14 -0
  15. package/dist/components/editor/components/SchemaInsertionModal.js +38 -0
  16. package/dist/components/editor/components/TableBubbleMenu.d.ts +6 -0
  17. package/dist/components/editor/components/TableBubbleMenu.js +8 -0
  18. package/dist/components/editor/extensions/Container.d.ts +2 -0
  19. package/dist/components/editor/extensions/Container.js +51 -0
  20. package/dist/components/editor/extensions/Grid.d.ts +3 -0
  21. package/dist/components/editor/extensions/Grid.js +89 -0
  22. package/dist/components/editor/extensions/Layout.d.ts +3 -0
  23. package/dist/components/editor/extensions/Layout.js +116 -0
  24. package/dist/components/editor/extensions/ResizableImage.d.ts +1 -0
  25. package/dist/components/editor/extensions/ResizableImage.js +52 -0
  26. package/dist/components/editor/extensions/SlashCommand.d.ts +15 -0
  27. package/dist/components/editor/extensions/SlashCommand.js +161 -0
  28. package/dist/components/editor/utils/upload.d.ts +1 -0
  29. package/dist/components/editor/utils/upload.js +49 -0
  30. package/dist/editor.css +460 -0
  31. package/dist/lib/upload.d.ts +1 -0
  32. package/dist/lib/upload.js +53 -0
  33. package/dist/lib/utils.d.ts +2 -0
  34. package/dist/lib/utils.js +5 -0
  35. package/dist/nextmin.css +1 -1
  36. package/dist/router/NextMinRouter.d.ts +1 -0
  37. package/dist/router/NextMinRouter.js +1 -0
  38. package/dist/views/list/useListData.js +4 -0
  39. package/package.json +34 -8
  40. package/tsconfig.json +8 -3
@@ -12,6 +12,10 @@ import { FileUploader } from './FileUploader';
12
12
  import AddressAutocompleteGoogle from './AddressAutocomplete';
13
13
  import { useGoogleMapsKey } from '../hooks/useGoogleMapsKey';
14
14
  import { parseDate, parseDateTime, parseTime, } from '@internationalized/date';
15
+ // import RichTextEditor from './editor/RichTextEditor';
16
+ // import ShadcnEditor from './editor/ShadcnEditor';
17
+ // import LexicalEditor from './editor/LexicalEditor';
18
+ import TiptapEditor from './editor/TiptapEditor';
15
19
  const inputClassNames = { inputWrapper: 'bg-transparent shadow-none' };
16
20
  const selectClassNames = { trigger: 'bg-transparent shadow-none' };
17
21
  const AUDIT_FIELDS = new Set(['createdAt', 'updatedAt']);
@@ -401,10 +405,15 @@ export function SchemaForm({ model, schemaOverride, initialValues, submitLabel =
401
405
  ? form[name]
402
406
  : ''
403
407
  : (form[name] ?? (Array.isArray(attr) ? [] : ''));
404
- return (_jsx("div", { className: colClass, children: _jsx(SchemaField, { uid: formUid, name: name, attr: attr, value: baseValue, onChange: handleChange, disabled: busy, inputClassNames: inputClassNames, selectClassNames: selectClassNames, mapsKey: mapsKey }) }, name));
408
+ // If it's a rich text field, force full width
409
+ const isRich = attr?.rich === true;
410
+ const dynamicColClass = isRich ? 'col-span-2' : colClass;
411
+ return (_jsx("div", { className: dynamicColClass, children: _jsx(SchemaField, { uid: formUid, name: name, attr: attr, inputClassNames: inputClassNames, selectClassNames: selectClassNames, value: baseValue, onChange: handleChange, disabled: busy,
412
+ // pass schemas for slash cmd
413
+ availableSchemas: items, mapsKey: mapsKey }) }, name));
405
414
  }), error && (_jsx("div", { className: "col-span-2", children: _jsx("div", { className: "text-danger text-sm", children: error }) })), _jsxs("div", { className: "flex gap-2 col-span-2", children: [_jsx(Button, { type: "submit", color: "primary", isLoading: busy, children: submitLabel }), showReset && (_jsx(Button, { type: "reset", variant: "flat", isDisabled: busy, children: "Reset" }))] })] }));
406
415
  }
407
- function SchemaField({ uid, name, attr, value, onChange, disabled, inputClassNames, selectClassNames, mapsKey, }) {
416
+ function SchemaField({ uid, name, attr, value, onChange, disabled, inputClassNames, selectClassNames, mapsKey, availableSchemas, }) {
408
417
  const id = `${uid}-${name}`;
409
418
  const label = attr?.label ?? formatLabel(name);
410
419
  const required = !!attr?.required;
@@ -416,6 +425,10 @@ function SchemaField({ uid, name, attr, value, onChange, disabled, inputClassNam
416
425
  const isPasswordField = name.toLowerCase() === 'password' ||
417
426
  attr?.format === 'password' ||
418
427
  attr?.writeOnly === true;
428
+ // RICH TEXT EDITOR CHECK
429
+ if (attr?.rich === true) {
430
+ return (_jsx(TiptapEditor, { value: value, onChange: (html) => onChange(name, html), placeholder: label, availableSchemas: availableSchemas }));
431
+ }
419
432
  const isPhoneField = isPhoneAttr(name, attr);
420
433
  const rawMask = typeof attr?.mask === 'string' ? attr.mask : '';
421
434
  const hasSlots = /[Xx9#_]/.test(rawMask);
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ type SuggestionProps = {
3
+ items: any[];
4
+ command: (item: any) => void;
5
+ };
6
+ export declare const SchemaSuggestionList: React.ForwardRefExoticComponent<SuggestionProps & React.RefAttributes<unknown>>;
7
+ export {};
@@ -0,0 +1,43 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
4
+ import { Card, Listbox, ListboxItem } from '@heroui/react';
5
+ export const SchemaSuggestionList = forwardRef((props, ref) => {
6
+ const [selectedIndex, setSelectedIndex] = useState(0);
7
+ const selectItem = (index) => {
8
+ const item = props.items[index];
9
+ if (item) {
10
+ props.command(item);
11
+ }
12
+ };
13
+ useEffect(() => {
14
+ setSelectedIndex(0);
15
+ }, [props.items]);
16
+ useImperativeHandle(ref, () => ({
17
+ onKeyDown: ({ event }) => {
18
+ if (event.key === 'ArrowUp') {
19
+ setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
20
+ return true;
21
+ }
22
+ if (event.key === 'ArrowDown') {
23
+ setSelectedIndex((selectedIndex + 1) % props.items.length);
24
+ return true;
25
+ }
26
+ if (event.key === 'Enter') {
27
+ selectItem(selectedIndex);
28
+ return true;
29
+ }
30
+ return false;
31
+ },
32
+ }));
33
+ if (!props.items?.length) {
34
+ return null;
35
+ }
36
+ return (_jsx(Card, { className: "min-w-[200px] overflow-hidden p-1 shadow-medium border border-default-200", children: _jsx(Listbox, { variant: "flat", "aria-label": "Schema suggestions", onAction: (key) => {
37
+ const index = props.items.findIndex(i => i.title === key);
38
+ selectItem(index);
39
+ }, children: props.items.map((item, index) => (_jsx(ListboxItem, {
40
+ // @ts-ignore
41
+ className: `${index === selectedIndex ? 'bg-default-100' : ''}`, startContent: _jsx("span", { className: "text-xl", children: "\uD83D\uDCC4" }), children: item.title }, item.title))) }) }));
42
+ });
43
+ SchemaSuggestionList.displayName = 'SchemaSuggestionList';
@@ -0,0 +1,10 @@
1
+ import { SchemaDef } from '../../lib/types';
2
+ interface TiptapEditorProps {
3
+ value: string;
4
+ onChange: (value: string) => void;
5
+ className?: string;
6
+ placeholder?: string;
7
+ availableSchemas?: SchemaDef[];
8
+ }
9
+ export declare const TiptapEditor: ({ value, onChange, className, placeholder, availableSchemas }: TiptapEditorProps) => import("react/jsx-runtime").JSX.Element;
10
+ export default TiptapEditor;
@@ -0,0 +1,228 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React, { useEffect, useState } from 'react';
3
+ import { Color } from '@tiptap/extension-color';
4
+ import { TextStyle } from '@tiptap/extension-text-style';
5
+ import Highlight from '@tiptap/extension-highlight';
6
+ import { useEditor, EditorContent } from '@tiptap/react';
7
+ import StarterKit from '@tiptap/starter-kit';
8
+ import Link from '@tiptap/extension-link';
9
+ import { api } from '../../lib/api';
10
+ import Underline from '@tiptap/extension-underline';
11
+ import TextAlign from '@tiptap/extension-text-align';
12
+ import Placeholder from '@tiptap/extension-placeholder';
13
+ import { Table } from '@tiptap/extension-table';
14
+ import TableRow from '@tiptap/extension-table-row';
15
+ import TableCell from '@tiptap/extension-table-cell';
16
+ import TableHeader from '@tiptap/extension-table-header';
17
+ import { ResizableImage } from './extensions/ResizableImage';
18
+ import { GridContainer, GridItem } from './extensions/Grid';
19
+ import { LayoutRow, LayoutColumn } from './extensions/Layout';
20
+ import { Toolbar } from './Toolbar';
21
+ import { cn } from '../../lib/utils';
22
+ // Styles for Tiptap
23
+ // Styles for Tiptap
24
+ // import '../../styles.css'; // REMOVED: CSS should be imported by consumer (see package.json exports)
25
+ import { SlashCommand, getSuggestionOptions } from './extensions/SlashCommand';
26
+ // ... imports
27
+ import { SchemaInsertionModal } from './components/SchemaInsertionModal';
28
+ import { ImageBubbleMenu } from './components/ImageBubbleMenu';
29
+ import { TableBubbleMenu } from './components/TableBubbleMenu';
30
+ import { Container } from './extensions/Container';
31
+ // ... interface
32
+ export const TiptapEditor = ({ value, onChange, className, placeholder = 'Start writing...', availableSchemas = [] }) => {
33
+ // State for modal
34
+ const [selectedSchema, setSelectedSchema] = useState(null);
35
+ const [isModalOpen, setIsModalOpen] = useState(false);
36
+ // Filter restricted schemas
37
+ const filteredSchemas = React.useMemo(() => {
38
+ const restricted = ['Roles', 'Settings', 'Users'];
39
+ return availableSchemas.filter(s => !restricted.includes(s.modelName));
40
+ }, [availableSchemas]);
41
+ const extensions = React.useMemo(() => [
42
+ StarterKit.configure({
43
+ heading: {
44
+ levels: [1, 2, 3]
45
+ }
46
+ }),
47
+ Highlight.configure({
48
+ multicolor: true,
49
+ }),
50
+ TextStyle,
51
+ Color,
52
+ Link.configure({
53
+ openOnClick: false,
54
+ HTMLAttributes: {
55
+ class: 'editor-link'
56
+ }
57
+ }),
58
+ Underline,
59
+ TextAlign.configure({
60
+ types: ['heading', 'paragraph', 'image'],
61
+ }),
62
+ ResizableImage,
63
+ GridContainer,
64
+ GridItem,
65
+ Container,
66
+ LayoutRow,
67
+ LayoutColumn,
68
+ Table.configure({
69
+ resizable: true,
70
+ }),
71
+ TableRow,
72
+ TableHeader,
73
+ TableCell,
74
+ Placeholder.configure({
75
+ placeholder,
76
+ }),
77
+ SlashCommand.configure({
78
+ suggestion: getSuggestionOptions(filteredSchemas, (item) => {
79
+ setSelectedSchema(item);
80
+ setIsModalOpen(true);
81
+ })
82
+ })
83
+ ], [placeholder, filteredSchemas]);
84
+ const editor = useEditor({
85
+ extensions,
86
+ content: value,
87
+ editorProps: {
88
+ attributes: {
89
+ class: cn("prose prose-sm sm:prose-base dark:prose-invert focus:outline-none h-[500px] overflow-y-auto p-4 max-w-none [&_li_p]:m-0", className)
90
+ }
91
+ },
92
+ onUpdate: ({ editor }) => {
93
+ onChange(editor.getHTML());
94
+ },
95
+ immediatelyRender: false,
96
+ });
97
+ // ... useEffect for sync ...
98
+ useEffect(() => {
99
+ if (editor && value !== editor.getHTML()) {
100
+ if (editor.getText() === '' && value === '')
101
+ return;
102
+ if (!editor.isFocused) {
103
+ setTimeout(() => {
104
+ editor.commands.setContent(value);
105
+ }, 0);
106
+ }
107
+ }
108
+ }, [value, editor]);
109
+ const handleSchemaInsert = async ({ viewType, template, ids = [], urlPattern }) => {
110
+ if (!editor || !selectedSchema)
111
+ return;
112
+ try {
113
+ let items = [];
114
+ if (ids.length > 0) {
115
+ // Fetch specific IDs
116
+ // Since api.list might not support ids array directly in all implementations,
117
+ // and we don't have a bulk get endpoint guaranteed, assume we can parallel fetch or filter.
118
+ // Optimally: user search results are already in RefMultiSelect but we don't have access to them here easily.
119
+ // We'll simplisticly fetch them in parallel.
120
+ // In production, an API like /model?ids=a,b,c is better.
121
+ // Let's assume we can fetch them one by one for now (up to ~10-20 is fine).
122
+ const promises = ids.map(id => api.get(selectedSchema.modelName, id).then((r) => r.data || r).catch(() => null));
123
+ const results = await Promise.all(promises);
124
+ items = results.filter(Boolean);
125
+ }
126
+ else {
127
+ // Fallback to latest 6
128
+ const res = await api.list(selectedSchema.modelName, 0, 6);
129
+ const payload = res?.data ?? res;
130
+ items =
131
+ payload?.items ??
132
+ payload?.docs ??
133
+ payload?.results ??
134
+ payload?.list ??
135
+ (Array.isArray(payload) ? payload : []);
136
+ items = items.slice(0, 6);
137
+ }
138
+ const displayItems = items;
139
+ const baseFrontendUrl = process.env.NEXT_PUBLIC_FRONTEND_URL || '';
140
+ // Helper to interpolate string with item data
141
+ const processTemplate = (tmpl, item) => {
142
+ let output = tmpl || selectedSchema.modelName;
143
+ output = output.replace(/{(\w+)}/g, (match, key) => {
144
+ return item[key] !== undefined && item[key] !== null ? String(item[key]) : match;
145
+ });
146
+ return output;
147
+ };
148
+ // Helper for URL
149
+ const processUrl = (pattern, item) => {
150
+ const suffix = processTemplate(pattern || '{slug}_{id}', item);
151
+ // Ensure base ends with / if needed? Usually env var doesn't have it.
152
+ // Or Pattern starts with /?
153
+ // User said "add another input that I can configure the url parts too AFTER the base url part"
154
+ // Assuming base url is domain.com/
155
+ // Join them carefully.
156
+ const cleanBase = baseFrontendUrl.replace(/\/$/, '');
157
+ const cleanSuffix = suffix.replace(/^\//, '');
158
+ return `${cleanBase}/${cleanSuffix}`;
159
+ };
160
+ let html = ''; // Add new line first
161
+ // Helper to interpolate string with item data
162
+ // const processTemplate = (tmpl: string, item: any) => {
163
+ // let output = tmpl || selectedSchema.modelName;
164
+ // output = output.replace(/{(\w+)}/g, (match, key) => {
165
+ // return item[key] !== undefined && item[key] !== null ? String(item[key]) : match;
166
+ // });
167
+ // return output;
168
+ // };
169
+ const gridClass = "grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 my-4";
170
+ const listClass = "flex flex-col gap-2 my-4";
171
+ // Link to styling ...
172
+ // For Grid: Use our custom GridContainer/GridItem nodes via data-type
173
+ // For List: Use standard divs (if supported) or just paragraphs?
174
+ // Actually, we can use the same Card style for list but in a single column?
175
+ // Tiptap might strip 'flex-col' div if we don't have a node for it.
176
+ // Let's use GridContainer with a modifier or just a standard behavior?
177
+ // The GridContainer is hardcoded to 3 cols.
178
+ // Responsive grid covers the list view mainly (mobile).
179
+ // But if user wants explicit List, maybe we use a different structure.
180
+ // For now, let's focus on GRID as requested.
181
+ // If viewType is 'list', we might just insert paragraphs or a different styled container.
182
+ // Actually, if we want list view, we can style the container?
183
+ // But GridContainer class is hardcoded in the Node extension.
184
+ // We can pass attributes?
185
+ // "data-cols"="1"?
186
+ // But I didn't verify dynamic classes in extension.
187
+ // Let's stick to Grid for 'grid' view type.
188
+ // For 'list', maybe we can just use paragraphs for now, or the same Grid with 1 column?
189
+ // I'll assume 'grid' is the priority.
190
+ if (viewType === 'grid') {
191
+ html = `<div data-type="grid-container">`;
192
+ if (displayItems.length === 0) {
193
+ html += `<div data-type="grid-item" href="#">No items found</div>`;
194
+ }
195
+ else {
196
+ displayItems.forEach(item => {
197
+ const text = processTemplate(template, item);
198
+ const url = processUrl(urlPattern || '', item);
199
+ html += `<div data-type="grid-item" href="${url}">${text}</div>`;
200
+ });
201
+ }
202
+ html += `</div>`; // Exit block
203
+ }
204
+ else {
205
+ // List View - use standard markup which might be safer or just paragraphs
206
+ html = `<p><strong>${selectedSchema.modelName} List:</strong></p><ul class="list-disc pl-5">`;
207
+ displayItems.forEach(item => {
208
+ const text = processTemplate(template, item);
209
+ const url = processUrl(urlPattern || '', item);
210
+ html += `<li><a href="${url}" target="_blank">${text}</a></li>`;
211
+ });
212
+ html += `</ul>`;
213
+ }
214
+ // Insert spacing first securely
215
+ editor.chain().focus()
216
+ .insertContent('<p>&nbsp;</p>') // Use non-breaking space to ensure block creation
217
+ .insertContent(html)
218
+ .run();
219
+ }
220
+ catch (err) {
221
+ console.error("Failed to fetch schema data", err);
222
+ // Fallback error message in editor?
223
+ editor.chain().focus().insertContent(`<p class="text-red-500">Error loading data for ${selectedSchema.modelName}</p>`).run();
224
+ }
225
+ };
226
+ return (_jsxs("div", { className: "border border-gray-200 dark:border-gray-800 rounded-lg overflow-hidden bg-white dark:bg-zinc-950 shadow-sm transition-all hover:border-gray-300 dark:hover:border-gray-700", children: [_jsx(Toolbar, { editor: editor }), _jsx(EditorContent, { editor: editor }), _jsx(ImageBubbleMenu, { editor: editor }), _jsx(TableBubbleMenu, { editor: editor }), _jsx(SchemaInsertionModal, { isOpen: isModalOpen, onClose: () => setIsModalOpen(false), onInsert: handleSchemaInsert, schema: selectedSchema })] }));
227
+ };
228
+ export default TiptapEditor;
@@ -0,0 +1,6 @@
1
+ import { type Editor } from '@tiptap/react';
2
+ interface ToolbarProps {
3
+ editor: Editor | null;
4
+ }
5
+ export declare const Toolbar: ({ editor }: ToolbarProps) => import("react/jsx-runtime").JSX.Element | null;
6
+ export {};
@@ -0,0 +1,99 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useRef } from 'react';
3
+ import { Bold, Italic, Underline as UnderlineIcon, List, ListOrdered, Quote, Code, Undo, Redo, Image as ImageIcon, LayoutTemplate, LayoutPanelLeft, AlignLeft, AlignCenter, AlignRight, Square, Table as TableIcon, Palette, PaintBucket, Type } from 'lucide-react';
4
+ import { cn } from '../../lib/utils';
5
+ import { uploadFile } from '../../lib/upload';
6
+ export const Toolbar = ({ editor }) => {
7
+ const fileInputRef = useRef(null);
8
+ if (!editor) {
9
+ return null;
10
+ }
11
+ const addImage = async (e) => {
12
+ const file = e.target.files?.[0];
13
+ if (file) {
14
+ try {
15
+ // Upload logic here - reusing the one we discussed or a simple URL creator for now
16
+ // For production, use the real uploadFile utility
17
+ // const url = URL.createObjectURL(file);
18
+ const url = await uploadFile(file); // Needs verify this exists/works
19
+ if (url) {
20
+ editor.chain().focus().setImage({ src: url }).run();
21
+ }
22
+ }
23
+ catch (error) {
24
+ console.error("Upload failed", error);
25
+ }
26
+ }
27
+ // Reset inputs
28
+ if (fileInputRef.current)
29
+ fileInputRef.current.value = '';
30
+ };
31
+ const handleImageClick = () => {
32
+ fileInputRef.current?.click();
33
+ };
34
+ const ToolbarButton = ({ onClick, isActive = false, children, disabled = false }) => (_jsx("button", { onClick: onClick, disabled: disabled, className: cn("p-2 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors", isActive && "bg-gray-200 dark:bg-gray-700 text-blue-500", disabled && "opacity-50 cursor-not-allowed"), type: "button", children: children }));
35
+ return (_jsxs("div", { className: "border-b border-gray-200 dark:border-gray-800 p-2 flex flex-wrap gap-1 sticky top-0 bg-white dark:bg-zinc-950 z-10 items-center", children: [_jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleBold().run(), isActive: editor.isActive('bold'), children: _jsx(Bold, { size: 18 }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleItalic().run(), isActive: editor.isActive('italic'), children: _jsx(Italic, { size: 18 }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleUnderline().run(), isActive: editor.isActive('underline'), children: _jsx(UnderlineIcon, { size: 18 }) }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().setTextAlign('left').run(), isActive: editor.isActive({ textAlign: 'left' }), children: _jsx(AlignLeft, { size: 18 }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().setTextAlign('center').run(), isActive: editor.isActive({ textAlign: 'center' }), children: _jsx(AlignCenter, { size: 18 }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().setTextAlign('right').run(), isActive: editor.isActive({ textAlign: 'right' }), children: _jsx(AlignRight, { size: 18 }) }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleHeading({ level: 1 }).run(), isActive: editor.isActive('heading', { level: 1 }), children: _jsx("span", { className: "font-bold text-sm", children: "H1" }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run(), isActive: editor.isActive('heading', { level: 2 }), children: _jsx("span", { className: "font-bold text-sm", children: "H2" }) }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleBulletList().run(), isActive: editor.isActive('bulletList'), children: _jsx(List, { size: 18 }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleOrderedList().run(), isActive: editor.isActive('orderedList'), children: _jsx(ListOrdered, { size: 18 }) }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleBlockquote().run(), isActive: editor.isActive('blockquote'), children: _jsx(Quote, { size: 18 }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().toggleCodeBlock().run(), isActive: editor.isActive('codeBlock'), children: _jsx(Code, { size: 18 }) }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx(ToolbarButton, { onClick: () => {
36
+ editor
37
+ .chain()
38
+ .focus()
39
+ .insertContent('<p>&nbsp;</p>')
40
+ .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>')
41
+ .run();
42
+ }, children: _jsx(LayoutTemplate, { size: 18 }) }), _jsx(ToolbarButton, { onClick: () => {
43
+ editor
44
+ .chain()
45
+ .focus()
46
+ .insertContent('<p>&nbsp;</p>')
47
+ .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>')
48
+ .run();
49
+ }, children: _jsx(LayoutPanelLeft, { size: 18 }) }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx(ToolbarButton, { onClick: () => {
50
+ editor
51
+ .chain()
52
+ .focus()
53
+ .insertContent({
54
+ type: 'container',
55
+ content: [
56
+ {
57
+ type: 'paragraph',
58
+ }
59
+ ]
60
+ })
61
+ .run();
62
+ }, children: _jsxs("div", { className: "flex flex-col items-center justify-center", children: [_jsx(Square, { size: 18 }), _jsx("span", { className: "text-[10px] leading-none", children: "Full" })] }) }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(), children: _jsx(TableIcon, { size: 18 }) }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsxs("div", { className: "relative group", children: [_jsx(ToolbarButton, { onClick: () => { }, isActive: editor.isActive('textStyle'), children: _jsx("div", { className: "flex items-center", children: _jsx(Type, { size: 18 }) }) }), _jsx("input", { type: "color", className: "absolute inset-0 opacity-0 w-full h-full cursor-pointer", onChange: (e) => editor.chain().focus().setColor(e.target.value).run(), value: editor.getAttributes('textStyle').color || '#000000' })] }), _jsxs("div", { className: "relative group", children: [_jsx(ToolbarButton, { onClick: () => { }, isActive: editor.isActive('highlight'), children: _jsx("div", { className: "flex items-center", children: _jsx(Palette, { size: 18 }) }) }), _jsx("input", { type: "color", className: "absolute inset-0 opacity-0 w-full h-full cursor-pointer", onChange: (e) => {
63
+ const color = e.target.value;
64
+ editor.chain().focus().setHighlight({ color }).run();
65
+ }, value: editor.getAttributes('highlight').color || '#ffff00' })] }), _jsxs("div", { className: "relative group", children: [_jsx(ToolbarButton, { onClick: () => { }, isActive: !!editor.getAttributes('gridContainer').backgroundColor || !!editor.getAttributes('layoutRow').backgroundColor || !!editor.getAttributes('layoutColumn').backgroundColor || !!editor.getAttributes('container').backgroundColor, children: _jsx(PaintBucket, { size: 18 }) }), _jsx("input", { type: "color", className: "absolute inset-0 opacity-0 w-full h-full cursor-pointer", onChange: (e) => {
66
+ const val = e.target.value;
67
+ // Priority: Grid Container (Apply to ALL) -> Container -> Column -> Row
68
+ if (editor.isActive('gridItem')) {
69
+ // Set on CONTAINER so it applies to all items via variable
70
+ editor.chain().focus().updateAttributes('gridContainer', { backgroundColor: val }).run();
71
+ }
72
+ else if (editor.isActive('container')) {
73
+ editor.chain().focus().updateAttributes('container', { backgroundColor: val }).run();
74
+ }
75
+ else if (editor.isActive('layoutColumn')) {
76
+ editor.chain().focus().updateAttributes('layoutColumn', { backgroundColor: val }).run();
77
+ }
78
+ else if (editor.isActive('layoutRow')) {
79
+ editor.chain().focus().updateAttributes('layoutRow', { backgroundColor: val }).run();
80
+ }
81
+ } })] })] }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsxs("div", { className: "flex items-center gap-1 relative group", children: [_jsx("span", { className: "text-[10px] text-gray-500 absolute left-1 pointer-events-none", children: "R" }), _jsx("input", { type: "number", placeholder: "0", className: "w-16 h-8 pl-4 pr-1 text-xs bg-transparent border border-gray-200 dark:border-gray-700 rounded", onChange: (e) => {
82
+ const val = e.target.value;
83
+ const radius = val ? `${val}px` : null;
84
+ const cmd = editor.chain().focus();
85
+ if (editor.isActive('gridItem') || editor.isActive('gridContainer')) {
86
+ cmd.updateAttributes('gridContainer', { borderRadius: radius });
87
+ }
88
+ else if (editor.isActive('container')) {
89
+ cmd.updateAttributes('container', { borderRadius: radius });
90
+ }
91
+ else if (editor.isActive('layoutColumn')) {
92
+ cmd.updateAttributes('layoutColumn', { borderRadius: radius });
93
+ }
94
+ else if (editor.isActive('layoutRow')) {
95
+ cmd.updateAttributes('layoutRow', { borderRadius: radius });
96
+ }
97
+ cmd.run();
98
+ } })] }), _jsx("div", { className: "w-[1px] h-6 bg-gray-300 dark:bg-gray-700 mx-1" }), _jsx(ToolbarButton, { onClick: handleImageClick, children: _jsx(ImageIcon, { size: 18 }) }), _jsx("input", { type: "file", ref: fileInputRef, onChange: addImage, className: "hidden", accept: "image/*" }), _jsx("div", { className: "flex-1" }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().undo().run(), disabled: !editor.can().undo(), children: _jsx(Undo, { size: 18 }) }), _jsx(ToolbarButton, { onClick: () => editor.chain().focus().redo().run(), disabled: !editor.can().redo(), children: _jsx(Redo, { size: 18 }) })] }));
99
+ };
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ import { SchemaDef } from '../../../lib/types';
3
+ export interface CommandListProps {
4
+ items: SchemaDef[];
5
+ command: (item: any) => void;
6
+ }
7
+ export declare const CommandList: React.ForwardRefExoticComponent<CommandListProps & React.RefAttributes<unknown>>;
@@ -0,0 +1,47 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState, useEffect, forwardRef, useImperativeHandle } from 'react';
3
+ import { cn } from '../../../lib/utils';
4
+ import { LayoutGrid } from 'lucide-react';
5
+ export const CommandList = forwardRef((props, ref) => {
6
+ const [selectedIndex, setSelectedIndex] = useState(0);
7
+ const selectItem = (index) => {
8
+ const item = props.items[index];
9
+ if (item) {
10
+ props.command(item);
11
+ }
12
+ };
13
+ const upHandler = () => {
14
+ setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
15
+ };
16
+ const downHandler = () => {
17
+ setSelectedIndex((selectedIndex + 1) % props.items.length);
18
+ };
19
+ const enterHandler = () => {
20
+ selectItem(selectedIndex);
21
+ };
22
+ useEffect(() => {
23
+ setSelectedIndex(0);
24
+ }, [props.items]);
25
+ useImperativeHandle(ref, () => ({
26
+ onKeyDown: ({ event }) => {
27
+ if (event.key === 'ArrowUp') {
28
+ upHandler();
29
+ return true;
30
+ }
31
+ if (event.key === 'ArrowDown') {
32
+ downHandler();
33
+ return true;
34
+ }
35
+ if (event.key === 'Enter') {
36
+ enterHandler();
37
+ return true;
38
+ }
39
+ return false;
40
+ },
41
+ }));
42
+ if (!props.items.length) {
43
+ return null;
44
+ }
45
+ return (_jsx("div", { className: "z-50 min-w-[250px] max-h-[300px] overflow-y-auto rounded-md border border-gray-200 bg-white shadow-md dark:border-gray-800 dark:bg-zinc-950 p-1", children: props.items.map((item, index) => (_jsxs("button", { className: cn("flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors", index === selectedIndex ? "bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-gray-50" : "text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800"), onClick: () => selectItem(index), children: [_jsx(LayoutGrid, { className: "h-4 w-4" }), _jsx("span", { className: "flex-1 text-left", children: item.modelName })] }, index))) }));
46
+ });
47
+ CommandList.displayName = 'CommandList';
@@ -0,0 +1,6 @@
1
+ import { Editor } from '@tiptap/react';
2
+ interface ImageBubbleMenuProps {
3
+ editor: Editor | null;
4
+ }
5
+ export declare const ImageBubbleMenu: ({ editor }: ImageBubbleMenuProps) => import("react/jsx-runtime").JSX.Element | null;
6
+ export {};
@@ -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,3 @@
1
+ import { NodeViewProps } from '@tiptap/react';
2
+ import React from 'react';
3
+ export declare const ImageComponent: React.FC<NodeViewProps>;
@@ -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,14 @@
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
+ }
13
+ export declare const SchemaInsertionModal: ({ isOpen, onClose, onInsert, schema, }: SchemaInsertionModalProps) => import("react/jsx-runtime").JSX.Element | null;
14
+ export {};