tetrons 2.2.1 → 2.2.3
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/app/layout.d.ts +1 -1
- package/dist/app/{layout.jsx → layout.js} +2 -5
- package/dist/app/page.d.ts +2 -2
- package/dist/app/page.js +6 -0
- package/dist/components/tetrons/EditorContent.d.ts +1 -2
- package/dist/components/tetrons/{EditorContent.jsx → EditorContent.js} +2 -24
- package/dist/components/tetrons/{ResizableImageComponent.jsx → ResizableImageComponent.js} +10 -12
- package/dist/components/tetrons/{ResizableVideoComponent.jsx → ResizableVideoComponent.js} +7 -8
- package/dist/components/tetrons/toolbar/ActionGroup.d.ts +1 -2
- package/dist/components/tetrons/toolbar/{ActionGroup.jsx → ActionGroup.js} +18 -39
- package/dist/components/tetrons/toolbar/ClipboardGroup.d.ts +1 -2
- package/dist/components/tetrons/toolbar/ClipboardGroup.js +31 -0
- package/dist/components/tetrons/toolbar/FileGroup.d.ts +1 -2
- package/dist/components/tetrons/toolbar/{FileGroup.jsx → FileGroup.js} +3 -6
- package/dist/components/tetrons/toolbar/FontStyleGroup.d.ts +1 -2
- package/dist/components/tetrons/toolbar/FontStyleGroup.js +63 -0
- package/dist/components/tetrons/toolbar/InsertGroup.d.ts +1 -2
- package/dist/components/tetrons/toolbar/InsertGroup.js +138 -0
- package/dist/components/tetrons/toolbar/ListAlignGroup.d.ts +1 -2
- package/dist/components/tetrons/toolbar/ListAlignGroup.js +7 -0
- package/dist/components/tetrons/toolbar/MiscGroup.d.ts +1 -2
- package/dist/components/tetrons/toolbar/MiscGroup.js +25 -0
- package/dist/components/tetrons/toolbar/TableContextMenu.d.ts +1 -2
- package/dist/components/tetrons/toolbar/{TableContextMenu.jsx → TableContextMenu.js} +3 -21
- package/dist/components/tetrons/toolbar/TetronsToolbar.d.ts +1 -2
- package/dist/components/tetrons/toolbar/{TetronsToolbar.jsx → TetronsToolbar.js} +2 -16
- package/dist/components/tetrons/toolbar/ToolbarButton.js +7 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.js +1 -0
- package/dist/index.mjs +731 -638
- package/dist/styles/styles/tetrons.css +371 -0
- package/dist/styles/tetrons.css +371 -0
- package/package.json +7 -4
- package/dist/app/page.jsx +0 -8
- package/dist/components/tetrons/toolbar/ClipboardGroup.jsx +0 -36
- package/dist/components/tetrons/toolbar/FontStyleGroup.jsx +0 -104
- package/dist/components/tetrons/toolbar/InsertGroup.jsx +0 -162
- package/dist/components/tetrons/toolbar/ListAlignGroup.jsx +0 -16
- package/dist/components/tetrons/toolbar/MiscGroup.jsx +0 -31
- package/dist/components/tetrons/toolbar/ToolbarButton.jsx +0 -8
- /package/dist/components/UI/{Button.jsx → Button.js} +0 -0
- /package/dist/components/UI/{Dropdown.jsx → Dropdown.js} +0 -0
package/dist/app/layout.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
2
|
import { Geist, Geist_Mono } from "next/font/google";
|
|
2
3
|
import "./globals.css";
|
|
3
4
|
const geistSans = Geist({
|
|
@@ -22,9 +23,5 @@ export const metadata = {
|
|
|
22
23
|
manifest: "/site.webmanifest",
|
|
23
24
|
};
|
|
24
25
|
export default function RootLayout({ children, }) {
|
|
25
|
-
return (
|
|
26
|
-
<body suppressHydrationWarning className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased bg-gray-50 text-gray-900`}>
|
|
27
|
-
<main className="max-w-screen-xl mx-auto p-4">{children}</main>
|
|
28
|
-
</body>
|
|
29
|
-
</html>);
|
|
26
|
+
return (_jsx("html", { lang: "en", children: _jsx("body", { suppressHydrationWarning: true, className: `${geistSans.variable} ${geistMono.variable} font-sans antialiased bg-gray-50 text-gray-900`, children: _jsx("main", { className: "max-w-screen-xl mx-auto p-4", children: children }) }) }));
|
|
30
27
|
}
|
package/dist/app/page.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
export default function Home(): import("react").JSX.Element;
|
|
1
|
+
import "../styles/tetrons.css";
|
|
2
|
+
export default function Home(): import("react/jsx-runtime").JSX.Element;
|
package/dist/app/page.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import EditorContent from "../components/tetrons/EditorContent";
|
|
3
|
+
import "../styles/tetrons.css";
|
|
4
|
+
export default function Home() {
|
|
5
|
+
return (_jsx("main", { className: "flex flex-col h-screen overflow-hidden", children: _jsx("div", { className: "flex-1 overflow-auto flex flex-col", children: _jsx(EditorContent, {}) }) }));
|
|
6
|
+
}
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export default function EditorContent(): React.JSX.Element;
|
|
1
|
+
export default function EditorContent(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
3
|
import React from "react";
|
|
3
4
|
import { Comment } from "./toolbar/extensions/Comment";
|
|
4
5
|
import { useEffect, useRef } from "react";
|
|
@@ -133,28 +134,5 @@ export default function EditorContent() {
|
|
|
133
134
|
setCurrentVersionIndex(index);
|
|
134
135
|
}
|
|
135
136
|
};
|
|
136
|
-
return (
|
|
137
|
-
<div className="p-2 border-b border-gray-300 bg-gray-50 flex flex-wrap items-center gap-3">
|
|
138
|
-
<button onClick={saveVersion} disabled={!editor} className="px-3 py-1 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50">
|
|
139
|
-
Save Version
|
|
140
|
-
</button>
|
|
141
|
-
|
|
142
|
-
<div className="flex items-center gap-2 overflow-x-auto max-w-full">
|
|
143
|
-
{versions.length === 0 && (<span className="text-gray-500 text-sm">No saved versions</span>)}
|
|
144
|
-
{versions.map((_, idx) => (<button key={idx} onClick={() => restoreVersion(idx)} className={`px-2 py-1 rounded border ${idx === currentVersionIndex
|
|
145
|
-
? "border-blue-600 font-semibold text-blue-600"
|
|
146
|
-
: "border-gray-300 text-gray-700 hover:border-gray-600"}`} title={`Restore Version ${idx + 1}`}>
|
|
147
|
-
{`V${idx + 1}`}
|
|
148
|
-
</button>))}
|
|
149
|
-
</div>
|
|
150
|
-
</div>
|
|
151
|
-
|
|
152
|
-
{editor && <TetronsToolbar editor={editor}/>}
|
|
153
|
-
<div ref={wrapperRef} className="flex-grow p-4 md:p-6 bg-white border border-gray-300 rounded shadow-sm overflow-auto min-h-0 prose relative" onClick={handleEditorClick}>
|
|
154
|
-
{editor ? (<>
|
|
155
|
-
<TiptapEditorContent editor={editor}/>
|
|
156
|
-
{editor && <TableContextMenu editor={editor}/>}
|
|
157
|
-
</>) : (<div className="text-gray-500">Loading editor...</div>)}
|
|
158
|
-
</div>
|
|
159
|
-
</div>);
|
|
137
|
+
return (_jsxs("div", { className: "editor-container", children: [_jsxs("div", { className: "editor-toolbar", children: [_jsx("button", { type: "button", onClick: saveVersion, disabled: !editor, className: "editor-save-btn", children: "Save Version" }), _jsxs("div", { className: "editor-versions-wrapper", children: [versions.length === 0 && (_jsx("span", { className: "editor-no-versions", children: "No saved versions" })), versions.map((_, idx) => (_jsx("button", { type: "button", onClick: () => restoreVersion(idx), className: `editor-version-btn ${idx === currentVersionIndex ? "active" : ""}`, title: `Restore Version ${idx + 1}`, children: `V${idx + 1}` }, idx)))] })] }), editor && _jsx(TetronsToolbar, { editor: editor }), _jsx("div", { ref: wrapperRef, className: "editor-content-wrapper", onClick: handleEditorClick, children: editor ? (_jsxs(_Fragment, { children: [_jsx(TiptapEditorContent, { editor: editor }), editor && _jsx(TableContextMenu, { editor: editor })] })) : (_jsx("div", { className: "editor-loading", children: "Loading editor..." })) })] }));
|
|
160
138
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useEffect } from "react";
|
|
2
3
|
import { NodeViewWrapper } from "@tiptap/react";
|
|
3
4
|
const ResizableImageComponent = ({ node, updateAttributes, selected, }) => {
|
|
4
5
|
const { src, alt, title, width, height } = node.attrs;
|
|
@@ -16,22 +17,19 @@ const ResizableImageComponent = ({ node, updateAttributes, selected, }) => {
|
|
|
16
17
|
observer.observe(img);
|
|
17
18
|
return () => observer.disconnect();
|
|
18
19
|
}, [updateAttributes]);
|
|
19
|
-
return (
|
|
20
|
+
return (_jsx(NodeViewWrapper, { ref: wrapperRef, contentEditable: false, className: `resizable-image-wrapper ${selected ? "ProseMirror-selectednode" : ""}`, style: {
|
|
20
21
|
resize: "both",
|
|
21
22
|
overflow: "auto",
|
|
22
23
|
border: "1px solid #ccc",
|
|
23
24
|
padding: 2,
|
|
24
25
|
display: "inline-block",
|
|
25
26
|
maxWidth: "100%",
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
pointerEvents: "auto",
|
|
34
|
-
}} draggable={false}/>
|
|
35
|
-
</NodeViewWrapper>);
|
|
27
|
+
}, children: _jsx("img", { ref: imgRef, src: src, alt: alt !== null && alt !== void 0 ? alt : "", title: title !== null && title !== void 0 ? title : "", loading: "lazy", style: {
|
|
28
|
+
width: width ? `${width}px` : "auto",
|
|
29
|
+
height: height ? `${height}px` : "auto",
|
|
30
|
+
display: "block",
|
|
31
|
+
userSelect: "none",
|
|
32
|
+
pointerEvents: "auto",
|
|
33
|
+
}, draggable: false }) }));
|
|
36
34
|
};
|
|
37
35
|
export default ResizableImageComponent;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useEffect } from "react";
|
|
2
3
|
import { NodeViewWrapper } from "@tiptap/react";
|
|
3
4
|
const ResizableVideoComponent = ({ node, updateAttributes, selected, }) => {
|
|
4
5
|
const { src, controls, width, height } = node.attrs;
|
|
@@ -16,17 +17,15 @@ const ResizableVideoComponent = ({ node, updateAttributes, selected, }) => {
|
|
|
16
17
|
observer.observe(video);
|
|
17
18
|
return () => observer.disconnect();
|
|
18
19
|
}, [updateAttributes]);
|
|
19
|
-
return (
|
|
20
|
+
return (_jsx(NodeViewWrapper, { ref: wrapperRef, contentEditable: false, className: `resizable-video-wrapper ${selected ? "ProseMirror-selectednode" : ""}`, style: {
|
|
20
21
|
resize: "both",
|
|
21
22
|
overflow: "auto",
|
|
22
23
|
border: "1px solid #ccc",
|
|
23
24
|
padding: "2px",
|
|
24
25
|
display: "inline-block",
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}}/>
|
|
30
|
-
</NodeViewWrapper>);
|
|
26
|
+
}, children: _jsx("video", { ref: videoRef, src: src, controls: controls, style: {
|
|
27
|
+
width: width ? `${width}px` : "auto",
|
|
28
|
+
height: height ? `${height}px` : "auto",
|
|
29
|
+
} }) }));
|
|
31
30
|
};
|
|
32
31
|
export default ResizableVideoComponent;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import React from "react";
|
|
2
1
|
import { Editor } from "@tiptap/react";
|
|
3
2
|
type ActionGroupProps = {
|
|
4
3
|
editor: Editor;
|
|
5
4
|
};
|
|
6
|
-
export default function ActionGroup({ editor }: ActionGroupProps):
|
|
5
|
+
export default function ActionGroup({ editor }: ActionGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
7
6
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
4
|
import { MdZoomIn, MdZoomOut, MdPrint, MdSave, MdDownload, } from "react-icons/md";
|
|
4
5
|
import ToolbarButton from "./ToolbarButton";
|
|
5
6
|
export default function ActionGroup({ editor }) {
|
|
@@ -25,17 +26,19 @@ export default function ActionGroup({ editor }) {
|
|
|
25
26
|
const zoomIn = () => {
|
|
26
27
|
const element = document.querySelector(".ProseMirror");
|
|
27
28
|
if (element) {
|
|
28
|
-
const
|
|
29
|
+
const style = element.style;
|
|
30
|
+
const currentZoom = parseFloat(style.zoom || "1");
|
|
29
31
|
const next = Math.min(currentZoom + 0.1, 2);
|
|
30
|
-
|
|
32
|
+
style.zoom = next.toString();
|
|
31
33
|
}
|
|
32
34
|
};
|
|
33
35
|
const zoomOut = () => {
|
|
34
36
|
const element = document.querySelector(".ProseMirror");
|
|
35
37
|
if (element) {
|
|
36
|
-
const
|
|
38
|
+
const style = element.style;
|
|
39
|
+
const currentZoom = parseFloat(style.zoom || "1");
|
|
37
40
|
const next = Math.max(currentZoom - 0.1, 0.5);
|
|
38
|
-
|
|
41
|
+
style.zoom = next.toString();
|
|
39
42
|
}
|
|
40
43
|
};
|
|
41
44
|
const handlePrint = () => {
|
|
@@ -128,38 +131,14 @@ export default function ActionGroup({ editor }) {
|
|
|
128
131
|
link.click();
|
|
129
132
|
document.body.removeChild(link);
|
|
130
133
|
};
|
|
131
|
-
return (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
</button>
|
|
142
|
-
|
|
143
|
-
{dropdownOpen && (<div className="absolute z-10 mt-2 w-40 bg-white border rounded shadow-md">
|
|
144
|
-
<button type="button" onClick={() => {
|
|
145
|
-
setDropdownOpen(false);
|
|
146
|
-
handleDownloadPDF();
|
|
147
|
-
}} className="w-full text-left px-4 py-2 hover:bg-gray-100">
|
|
148
|
-
Export as PDF
|
|
149
|
-
</button>
|
|
150
|
-
<button type="button" onClick={() => {
|
|
151
|
-
setDropdownOpen(false);
|
|
152
|
-
handleDownloadHTML();
|
|
153
|
-
}} className="w-full text-left px-4 py-2 hover:bg-gray-100">
|
|
154
|
-
Export as HTML
|
|
155
|
-
</button>
|
|
156
|
-
<button type="button" onClick={() => {
|
|
157
|
-
setDropdownOpen(false);
|
|
158
|
-
handleDownloadDOCX();
|
|
159
|
-
}} className="w-full text-left px-4 py-2 hover:bg-gray-100">
|
|
160
|
-
Export as DOCX
|
|
161
|
-
</button>
|
|
162
|
-
</div>)}
|
|
163
|
-
</div>
|
|
164
|
-
</div>);
|
|
134
|
+
return (_jsxs("div", { className: "action-group", role: "group", "aria-label": "Editor actions", children: [_jsx(ToolbarButton, { icon: MdZoomIn, onClick: zoomIn, title: "Zoom In" }), _jsx(ToolbarButton, { icon: MdZoomOut, onClick: zoomOut, title: "Zoom Out" }), _jsx(ToolbarButton, { icon: MdPrint, onClick: handlePrint, title: "Print" }), _jsx(ToolbarButton, { icon: MdSave, onClick: handleSave, title: "Save" }), _jsxs("div", { className: "relative", ref: dropdownRef, children: [_jsxs("button", { type: "button", onClick: () => setDropdownOpen((open) => !open), "aria-haspopup": "menu", "aria-expanded": dropdownOpen ? "true" : "false", className: "export-button", title: "Export", children: [_jsx(MdDownload, {}), _jsx("span", { className: "text-sm" })] }), dropdownOpen && (_jsxs("div", { className: "export-dropdown", children: [_jsx("button", { type: "button", onClick: () => {
|
|
135
|
+
setDropdownOpen(false);
|
|
136
|
+
handleDownloadPDF();
|
|
137
|
+
}, children: "Export as PDF" }), _jsx("button", { type: "button", onClick: () => {
|
|
138
|
+
setDropdownOpen(false);
|
|
139
|
+
handleDownloadHTML();
|
|
140
|
+
}, children: "Export as HTML" }), _jsx("button", { type: "button", onClick: () => {
|
|
141
|
+
setDropdownOpen(false);
|
|
142
|
+
handleDownloadDOCX();
|
|
143
|
+
}, children: "Export as DOCX" })] }))] })] }));
|
|
165
144
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { MdContentPaste, MdContentCut, MdContentCopy, MdFormatPaint, } from "react-icons/md";
|
|
3
|
+
import ToolbarButton from "./ToolbarButton";
|
|
4
|
+
export default function ClipboardGroup({ editor }) {
|
|
5
|
+
return (_jsxs("div", { className: "clipboard-group", children: [_jsx(ToolbarButton, { icon: MdContentPaste, title: "Paste", onClick: async () => {
|
|
6
|
+
try {
|
|
7
|
+
const text = await navigator.clipboard.readText();
|
|
8
|
+
editor.chain().focus().insertContent(text).run();
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
console.error("Failed to read clipboard contents:", error);
|
|
12
|
+
}
|
|
13
|
+
} }), _jsx(ToolbarButton, { icon: MdContentCut, title: "Cut", onClick: () => {
|
|
14
|
+
const { from, to } = editor.state.selection;
|
|
15
|
+
if (from === to)
|
|
16
|
+
return;
|
|
17
|
+
const selectedText = editor.state.doc.textBetween(from, to);
|
|
18
|
+
navigator.clipboard.writeText(selectedText).then(() => {
|
|
19
|
+
editor.chain().focus().deleteRange({ from, to }).run();
|
|
20
|
+
});
|
|
21
|
+
} }), _jsx(ToolbarButton, { icon: MdContentCopy, title: "Copy", onClick: () => {
|
|
22
|
+
const { from, to } = editor.state.selection;
|
|
23
|
+
if (from === to)
|
|
24
|
+
return;
|
|
25
|
+
const selectedText = editor.state.doc.textBetween(from, to);
|
|
26
|
+
navigator.clipboard.writeText(selectedText);
|
|
27
|
+
} }), _jsx(ToolbarButton, { icon: MdFormatPaint, title: "Format Painter", onClick: () => {
|
|
28
|
+
const currentMarks = editor.getAttributes("textStyle");
|
|
29
|
+
localStorage.setItem("formatPainter", JSON.stringify(currentMarks));
|
|
30
|
+
} })] }));
|
|
31
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Editor } from "@tiptap/react";
|
|
2
|
-
import React from "react";
|
|
3
2
|
type FileGroupProps = {
|
|
4
3
|
editor: Editor;
|
|
5
4
|
};
|
|
6
|
-
export default function FileGroup({ editor }: FileGroupProps):
|
|
5
|
+
export default function FileGroup({ editor }: FileGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
7
6
|
export {};
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
3
|
import { FaRegFolderOpen } from "react-icons/fa";
|
|
3
4
|
import { VscNewFile } from "react-icons/vsc";
|
|
4
5
|
import ToolbarButton from "./ToolbarButton";
|
|
5
|
-
import
|
|
6
|
+
import { useRef } from "react";
|
|
6
7
|
export default function FileGroup({ editor }) {
|
|
7
8
|
const fileInputRef = useRef(null);
|
|
8
9
|
const handleNew = () => {
|
|
@@ -32,9 +33,5 @@ export default function FileGroup({ editor }) {
|
|
|
32
33
|
e.target.value = "";
|
|
33
34
|
}
|
|
34
35
|
};
|
|
35
|
-
return (
|
|
36
|
-
<input type="file" accept=".json" ref={fileInputRef} onChange={handleFileChange} className="hidden" aria-label="Open JSON file"/>
|
|
37
|
-
<ToolbarButton icon={VscNewFile} onClick={handleNew} title="New"/>
|
|
38
|
-
<ToolbarButton icon={FaRegFolderOpen} onClick={handleOpen} title="Open File"/>
|
|
39
|
-
</div>);
|
|
36
|
+
return (_jsxs("div", { className: "file-group", role: "group", "aria-label": "File actions", children: [_jsx("input", { type: "file", accept: ".json", ref: fileInputRef, onChange: handleFileChange, className: "hidden", "aria-label": "Open JSON file" }), _jsx(ToolbarButton, { icon: VscNewFile, onClick: handleNew, title: "New" }), _jsx(ToolbarButton, { icon: FaRegFolderOpen, onClick: handleOpen, title: "Open File" })] }));
|
|
40
37
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Editor } from "@tiptap/react";
|
|
2
|
-
import React from "react";
|
|
3
2
|
interface FontStyleGroupProps {
|
|
4
3
|
editor: Editor;
|
|
5
4
|
}
|
|
6
|
-
export default function FontStyleGroup({ editor }: FontStyleGroupProps):
|
|
5
|
+
export default function FontStyleGroup({ editor }: FontStyleGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
7
6
|
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { MdFormatBold, MdFormatItalic, MdFormatUnderlined, MdStrikethroughS, MdSubscript, MdSuperscript, MdFormatClear, MdFormatPaint, } from "react-icons/md";
|
|
4
|
+
import { ImTextColor } from "react-icons/im";
|
|
5
|
+
import { BiSolidColorFill } from "react-icons/bi";
|
|
6
|
+
import ToolbarButton from "./ToolbarButton";
|
|
7
|
+
import { useEffect, useState } from "react";
|
|
8
|
+
export default function FontStyleGroup({ editor }) {
|
|
9
|
+
const [textColor, setTextColor] = useState("#000000");
|
|
10
|
+
const [highlightColor, setHighlightColor] = useState("#ffff00");
|
|
11
|
+
const [fontFamily, setFontFamily] = useState("Arial");
|
|
12
|
+
const [fontSize, setFontSize] = useState("16px");
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!editor)
|
|
15
|
+
return;
|
|
16
|
+
const updateStates = () => {
|
|
17
|
+
var _a, _b, _c;
|
|
18
|
+
const highlight = editor.getAttributes("highlight");
|
|
19
|
+
setHighlightColor((highlight === null || highlight === void 0 ? void 0 : highlight.color) || "#ffff00");
|
|
20
|
+
const color = (_a = editor.getAttributes("textStyle")) === null || _a === void 0 ? void 0 : _a.color;
|
|
21
|
+
setTextColor(color || "#000000");
|
|
22
|
+
const fontAttr = ((_b = editor.getAttributes("fontFamily")) === null || _b === void 0 ? void 0 : _b.font) || "Arial";
|
|
23
|
+
setFontFamily(fontAttr);
|
|
24
|
+
const sizeAttr = ((_c = editor.getAttributes("fontSize")) === null || _c === void 0 ? void 0 : _c.size) || "16px";
|
|
25
|
+
setFontSize(sizeAttr);
|
|
26
|
+
};
|
|
27
|
+
updateStates();
|
|
28
|
+
editor.on("selectionUpdate", updateStates);
|
|
29
|
+
editor.on("transaction", updateStates);
|
|
30
|
+
return () => {
|
|
31
|
+
editor.off("selectionUpdate", updateStates);
|
|
32
|
+
editor.off("transaction", updateStates);
|
|
33
|
+
};
|
|
34
|
+
}, [editor]);
|
|
35
|
+
return (_jsxs("div", { className: "font-style-group", children: [_jsxs("select", { title: "Font Family", value: fontFamily, onChange: (e) => {
|
|
36
|
+
const value = e.target.value;
|
|
37
|
+
setFontFamily(value);
|
|
38
|
+
editor.chain().focus().setFontFamily(value).run();
|
|
39
|
+
}, children: [_jsx("option", { value: "Arial", children: "Arial" }), _jsx("option", { value: "Georgia", children: "Georgia" }), _jsx("option", { value: "Times New Roman", children: "Times New Roman" }), _jsx("option", { value: "Courier New", children: "Courier New" }), _jsx("option", { value: "Verdana", children: "Verdana" })] }), _jsxs("select", { title: "Font Size", value: fontSize, onChange: (e) => {
|
|
40
|
+
const value = e.target.value;
|
|
41
|
+
setFontSize(value);
|
|
42
|
+
editor.chain().focus().setFontSize(value).run();
|
|
43
|
+
}, children: [_jsx("option", { value: "12px", children: "12" }), _jsx("option", { value: "14px", children: "14" }), _jsx("option", { value: "16px", children: "16" }), _jsx("option", { value: "18px", children: "18" }), _jsx("option", { value: "24px", children: "24" }), _jsx("option", { value: "36px", children: "36" }), _jsx("option", { value: "48px", children: "48" }), _jsx("option", { value: "64px", children: "64" }), _jsx("option", { value: "72px", children: "72" })] }), _jsx(ToolbarButton, { icon: MdFormatBold, label: "Bold", onClick: () => editor.chain().focus().toggleBold().run(), isActive: editor.isActive("bold") }), _jsx(ToolbarButton, { icon: MdFormatItalic, label: "Italic", onClick: () => editor.chain().focus().toggleItalic().run(), isActive: editor.isActive("italic") }), _jsx(ToolbarButton, { icon: MdFormatUnderlined, label: "Underline", onClick: () => editor.chain().focus().toggleUnderline().run(), isActive: editor.isActive("underline") }), _jsx(ToolbarButton, { icon: MdStrikethroughS, label: "Strikethrough", onClick: () => editor.chain().focus().toggleStrike().run(), isActive: editor.isActive("strike") }), _jsx(ToolbarButton, { icon: MdSubscript, label: "Subscript", onClick: () => editor.chain().focus().toggleSubscript().run(), isActive: editor.isActive("subscript") }), _jsx(ToolbarButton, { icon: MdSuperscript, label: "Superscript", onClick: () => editor.chain().focus().toggleSuperscript().run(), isActive: editor.isActive("superscript") }), _jsxs("label", { title: "Font Color", "aria-label": "Font Color", className: "color-label", style: { "--indicator-color": textColor }, children: [_jsx(ImTextColor, { size: 20 }), _jsx("div", { className: "color-indicator" }), _jsx("input", { type: "color", value: textColor, onChange: (e) => {
|
|
44
|
+
const color = e.target.value;
|
|
45
|
+
setTextColor(color);
|
|
46
|
+
editor.chain().focus().setColor(color).run();
|
|
47
|
+
} })] }), _jsxs("label", { title: "Highlight Color", "aria-label": "Highlight Color", className: "color-label", style: { "--indicator-color": highlightColor }, children: [_jsx(BiSolidColorFill, { size: 20 }), _jsx("div", { className: "color-indicator" }), _jsx("input", { type: "color", value: highlightColor, onChange: (e) => {
|
|
48
|
+
const color = e.target.value;
|
|
49
|
+
setHighlightColor(color);
|
|
50
|
+
editor.chain().focus().setHighlight({ color }).run();
|
|
51
|
+
} })] }), _jsx(ToolbarButton, { icon: MdFormatClear, label: "Clear Formatting", onClick: () => editor.chain().focus().unsetAllMarks().run() }), _jsx(ToolbarButton, { icon: MdFormatPaint, label: "Apply Painter Format", onClick: () => {
|
|
52
|
+
const format = JSON.parse(localStorage.getItem("formatPainter") || "{}");
|
|
53
|
+
if (format.color)
|
|
54
|
+
editor.chain().focus().setColor(format.color).run();
|
|
55
|
+
if (format.backgroundColor) {
|
|
56
|
+
editor
|
|
57
|
+
.chain()
|
|
58
|
+
.focus()
|
|
59
|
+
.setHighlight({ color: format.backgroundColor })
|
|
60
|
+
.run();
|
|
61
|
+
}
|
|
62
|
+
} })] }));
|
|
63
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useRef, useState } from "react";
|
|
4
|
+
import { MdTableChart, MdInsertPhoto, MdInsertLink, MdInsertComment, MdInsertEmoticon, MdHorizontalRule, MdVideoLibrary, MdOutlineOndemandVideo, } from "react-icons/md";
|
|
5
|
+
import ToolbarButton from "./ToolbarButton";
|
|
6
|
+
import Picker from "@emoji-mart/react";
|
|
7
|
+
export default function InsertGroup({ editor }) {
|
|
8
|
+
const [showTableGrid, setShowTableGrid] = useState(false);
|
|
9
|
+
const [selectedRows, setSelectedRows] = useState(1);
|
|
10
|
+
const [selectedCols, setSelectedCols] = useState(1);
|
|
11
|
+
const imageInputRef = useRef(null);
|
|
12
|
+
const videoInputRef = useRef(null);
|
|
13
|
+
const [showPicker, setShowPicker] = useState(false);
|
|
14
|
+
const addEmoji = (emoji) => {
|
|
15
|
+
editor.chain().focus().insertContent(emoji.native).run();
|
|
16
|
+
setShowPicker(false);
|
|
17
|
+
};
|
|
18
|
+
const handleTableCellHover = (row, col) => {
|
|
19
|
+
setSelectedRows(row);
|
|
20
|
+
setSelectedCols(col);
|
|
21
|
+
};
|
|
22
|
+
const handleTableInsert = (rows, cols) => {
|
|
23
|
+
editor
|
|
24
|
+
.chain()
|
|
25
|
+
.focus()
|
|
26
|
+
.insertTable({
|
|
27
|
+
rows: rows || 1,
|
|
28
|
+
cols: cols || 1,
|
|
29
|
+
withHeaderRow: true,
|
|
30
|
+
})
|
|
31
|
+
.run();
|
|
32
|
+
setShowTableGrid(false);
|
|
33
|
+
setSelectedRows(1);
|
|
34
|
+
setSelectedCols(1);
|
|
35
|
+
};
|
|
36
|
+
const handleImageUpload = (e) => {
|
|
37
|
+
var _a;
|
|
38
|
+
const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
39
|
+
if (file) {
|
|
40
|
+
const reader = new FileReader();
|
|
41
|
+
reader.onload = () => {
|
|
42
|
+
editor
|
|
43
|
+
.chain()
|
|
44
|
+
.focus()
|
|
45
|
+
.setImage({ src: reader.result })
|
|
46
|
+
.run();
|
|
47
|
+
};
|
|
48
|
+
reader.readAsDataURL(file);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const handleVideoUpload = (e) => {
|
|
52
|
+
var _a;
|
|
53
|
+
const file = (_a = e.target.files) === null || _a === void 0 ? void 0 : _a[0];
|
|
54
|
+
if (file) {
|
|
55
|
+
const reader = new FileReader();
|
|
56
|
+
reader.onload = () => {
|
|
57
|
+
editor
|
|
58
|
+
.chain()
|
|
59
|
+
.focus()
|
|
60
|
+
.setVideo({ src: reader.result, controls: true })
|
|
61
|
+
.run();
|
|
62
|
+
};
|
|
63
|
+
reader.readAsDataURL(file);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
function normalizeEmbedUrl(url) {
|
|
67
|
+
try {
|
|
68
|
+
const u = new URL(url);
|
|
69
|
+
const hostname = u.hostname.replace("www.", "").toLowerCase();
|
|
70
|
+
if (hostname === "youtube.com" || hostname === "youtu.be") {
|
|
71
|
+
let videoId = u.searchParams.get("v");
|
|
72
|
+
if (!videoId && hostname === "youtu.be") {
|
|
73
|
+
videoId = u.pathname.slice(1);
|
|
74
|
+
}
|
|
75
|
+
if (videoId) {
|
|
76
|
+
return `https://www.youtube.com/embed/${videoId}`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (hostname === "vimeo.com") {
|
|
80
|
+
const videoId = u.pathname.split("/")[1];
|
|
81
|
+
if (videoId) {
|
|
82
|
+
return `https://player.vimeo.com/video/${videoId}`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (hostname === "google.com" ||
|
|
86
|
+
hostname === "maps.google.com" ||
|
|
87
|
+
hostname === "goo.gl") {
|
|
88
|
+
if (url.includes("/maps/embed")) {
|
|
89
|
+
return url;
|
|
90
|
+
}
|
|
91
|
+
return url.replace("/maps/", "/maps/embed/");
|
|
92
|
+
}
|
|
93
|
+
return url;
|
|
94
|
+
}
|
|
95
|
+
catch (_a) {
|
|
96
|
+
return url;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return (_jsxs("div", { className: "insert-group", children: [_jsx("input", { type: "file", accept: "image/*", ref: imageInputRef, className: "hidden-input", onChange: handleImageUpload, "aria-label": "Upload Image", title: "Upload Image" }), _jsx("input", { type: "file", accept: "video/*", ref: videoInputRef, className: "hidden-input", onChange: handleVideoUpload, "aria-label": "Upload Video", title: "Upload Video" }), _jsx(ToolbarButton, { icon: MdTableChart, label: "Insert Table", onClick: () => setShowTableGrid(!showTableGrid) }), showTableGrid && (_jsxs("div", { className: "table-grid-popup", onMouseLeave: () => setShowTableGrid(false), children: [_jsx("div", { className: "table-grid", children: [...Array(10)].map((_, row) => [...Array(10)].map((_, col) => {
|
|
100
|
+
const isSelected = row < selectedRows && col < selectedCols;
|
|
101
|
+
return (_jsx("div", { className: `table-grid-cell ${isSelected ? "selected" : ""}`, onMouseEnter: () => handleTableCellHover(row + 1, col + 1), onClick: () => handleTableInsert(row + 1, col + 1) }, `${row}-${col}`));
|
|
102
|
+
})) }), _jsxs("div", { className: "table-grid-label", children: [selectedRows, " x ", selectedCols] })] })), _jsx(ToolbarButton, { icon: MdInsertPhoto, label: "Insert Image", onClick: () => { var _a; return (_a = imageInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); } }), _jsx(ToolbarButton, { icon: MdVideoLibrary, label: "Insert Video", onClick: () => { var _a; return (_a = videoInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); } }), _jsx(ToolbarButton, { icon: MdInsertLink, label: "Insert Link", onClick: () => {
|
|
103
|
+
const url = prompt("Enter URL");
|
|
104
|
+
if (url) {
|
|
105
|
+
editor
|
|
106
|
+
.chain()
|
|
107
|
+
.focus()
|
|
108
|
+
.extendMarkRange("link")
|
|
109
|
+
.setLink({ href: url })
|
|
110
|
+
.run();
|
|
111
|
+
}
|
|
112
|
+
} }), _jsx(ToolbarButton, { icon: MdInsertComment, label: "Insert Comment", onClick: () => {
|
|
113
|
+
const comment = prompt("Enter your comment");
|
|
114
|
+
if (comment &&
|
|
115
|
+
editor.can().chain().focus().setComment(comment).run()) {
|
|
116
|
+
editor.chain().focus().setComment(comment).run();
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
console.warn("Cannot apply comment — maybe no selection?");
|
|
120
|
+
}
|
|
121
|
+
} }), _jsxs("div", { className: "relative", children: [_jsx(ToolbarButton, { icon: MdInsertEmoticon, label: "Emoji", onClick: () => setShowPicker(!showPicker) }), showPicker && (_jsx("div", { className: "emoji-picker", children: _jsx(Picker, { onEmojiSelect: addEmoji, theme: "auto", emoji: "point_up", showPreview: false, showSkinTones: true, emojiTooltip: true }) }))] }), _jsx(ToolbarButton, { icon: MdHorizontalRule, label: "Horizontal Line", onClick: () => editor.chain().focus().setHorizontalRule().run() }), _jsx(ToolbarButton, { icon: MdOutlineOndemandVideo, label: "Embed", onClick: () => {
|
|
122
|
+
const url = prompt("Enter embed URL (YouTube, Vimeo, Google Maps, etc.)");
|
|
123
|
+
if (!url)
|
|
124
|
+
return;
|
|
125
|
+
const embedUrl = normalizeEmbedUrl(url);
|
|
126
|
+
const width = prompt("Enter width in pixels", "560");
|
|
127
|
+
const height = prompt("Enter height in pixels", "315");
|
|
128
|
+
editor
|
|
129
|
+
.chain()
|
|
130
|
+
.focus()
|
|
131
|
+
.setEmbed({
|
|
132
|
+
src: embedUrl,
|
|
133
|
+
width: width ? parseInt(width) : 560,
|
|
134
|
+
height: height ? parseInt(height) : 315,
|
|
135
|
+
})
|
|
136
|
+
.run();
|
|
137
|
+
} })] }));
|
|
138
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { MdFormatListBulleted, MdFormatListNumbered, MdFormatIndentDecrease, MdFormatIndentIncrease, MdFormatAlignLeft, MdFormatAlignCenter, MdFormatAlignRight, MdFormatAlignJustify, } from "react-icons/md";
|
|
4
|
+
import ToolbarButton from "./ToolbarButton";
|
|
5
|
+
export default function ListAlignGroup({ editor }) {
|
|
6
|
+
return (_jsxs("div", { className: "list-align-group", children: [_jsx(ToolbarButton, { icon: MdFormatListBulleted, title: "Bulleted List", onClick: () => editor.chain().focus().toggleBulletList().run(), disabled: !editor.can().toggleBulletList() }), _jsx(ToolbarButton, { icon: MdFormatListNumbered, title: "Numbered List", onClick: () => editor.chain().focus().toggleOrderedList().run(), disabled: !editor.can().toggleOrderedList() }), _jsx(ToolbarButton, { icon: MdFormatIndentIncrease, title: "Increase Indent", onClick: () => editor.chain().focus().sinkListItem("listItem").run(), disabled: !editor.can().sinkListItem("listItem") }), _jsx(ToolbarButton, { icon: MdFormatIndentDecrease, title: "Decrease Indent", onClick: () => editor.chain().focus().liftListItem("listItem").run(), disabled: !editor.can().liftListItem("listItem") }), _jsx(ToolbarButton, { icon: MdFormatAlignLeft, title: "Align Left", onClick: () => editor.chain().focus().setTextAlign("left").run(), disabled: !editor.can().setTextAlign("left") }), _jsx(ToolbarButton, { icon: MdFormatAlignCenter, title: "Align Center", onClick: () => editor.chain().focus().setTextAlign("center").run(), disabled: !editor.can().setTextAlign("center") }), _jsx(ToolbarButton, { icon: MdFormatAlignRight, title: "Align Right", onClick: () => editor.chain().focus().setTextAlign("right").run(), disabled: !editor.can().setTextAlign("right") }), _jsx(ToolbarButton, { icon: MdFormatAlignJustify, title: "Justify", onClick: () => editor.chain().focus().setTextAlign("justify").run(), disabled: !editor.can().setTextAlign("justify") })] }));
|
|
7
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import React from "react";
|
|
2
1
|
import { Editor } from "@tiptap/react";
|
|
3
2
|
interface MiscGroupProps {
|
|
4
3
|
editor: Editor;
|
|
5
4
|
}
|
|
6
|
-
export default function MiscGroup({ editor }: MiscGroupProps):
|
|
5
|
+
export default function MiscGroup({ editor }: MiscGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
7
6
|
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { MdUndo, MdRedo, MdRefresh, MdVisibility, MdCode, } from "react-icons/md";
|
|
3
|
+
import ToolbarButton from "./ToolbarButton";
|
|
4
|
+
export default function MiscGroup({ editor }) {
|
|
5
|
+
const handlePreview = () => {
|
|
6
|
+
const html = editor.getHTML();
|
|
7
|
+
const previewWindow = window.open("", "_blank");
|
|
8
|
+
if (previewWindow) {
|
|
9
|
+
previewWindow.document.open();
|
|
10
|
+
previewWindow.document.write(`
|
|
11
|
+
<html>
|
|
12
|
+
<head>
|
|
13
|
+
<title>Preview</title>
|
|
14
|
+
<style>
|
|
15
|
+
body { font-family: sans-serif; padding: 2rem; line-height: 1.6; }
|
|
16
|
+
</style>
|
|
17
|
+
</head>
|
|
18
|
+
<body>${html}</body>
|
|
19
|
+
</html>
|
|
20
|
+
`);
|
|
21
|
+
previewWindow.document.close();
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
return (_jsxs("div", { className: "misc-group", children: [_jsx(ToolbarButton, { icon: MdUndo, label: "Undo", onClick: () => editor.chain().focus().undo().run(), disabled: !editor.can().undo() }), _jsx(ToolbarButton, { icon: MdRedo, label: "Redo", onClick: () => editor.chain().focus().redo().run(), disabled: !editor.can().redo() }), _jsx(ToolbarButton, { icon: MdRefresh, label: "Reset Formatting", onClick: () => editor.chain().focus().unsetAllMarks().clearNodes().run() }), _jsx(ToolbarButton, { icon: MdCode, label: "Toggle Code Block", onClick: () => editor.chain().focus().toggleCodeBlock().run(), isActive: editor.isActive("codeBlock") }), _jsx(ToolbarButton, { icon: MdVisibility, label: "Preview", onClick: handlePreview })] }));
|
|
25
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import React from "react";
|
|
2
1
|
import { Editor } from "@tiptap/react";
|
|
3
2
|
interface ContextMenuProps {
|
|
4
3
|
editor: Editor;
|
|
5
4
|
}
|
|
6
|
-
export default function TableContextMenu({ editor }: ContextMenuProps):
|
|
5
|
+
export default function TableContextMenu({ editor }: ContextMenuProps): import("react/jsx-runtime").JSX.Element | null;
|
|
7
6
|
export {};
|