tetrons 2.3.22 → 2.3.24

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 (41) hide show
  1. package/dist/app/page.d.ts +2 -0
  2. package/dist/app/page.jsx +51 -0
  3. package/dist/components/UI/Button.tsx +0 -0
  4. package/dist/components/UI/Dropdown.tsx +0 -0
  5. package/dist/components/components/tetrons/EditorContent.tsx +60 -62
  6. package/dist/components/tetrons/EditorContent.d.ts +6 -0
  7. package/dist/components/tetrons/EditorContent.tsx +280 -0
  8. package/dist/components/tetrons/ResizableImage.ts +39 -0
  9. package/dist/components/tetrons/ResizableImageComponent.tsx +77 -0
  10. package/dist/components/tetrons/ResizableVideo.ts +66 -0
  11. package/dist/components/tetrons/ResizableVideoComponent.tsx +56 -0
  12. package/dist/components/tetrons/extensions/Spellcheck.ts +50 -0
  13. package/dist/components/tetrons/helpers.ts +0 -0
  14. package/dist/components/tetrons/toolbar/ActionGroup.tsx +218 -0
  15. package/dist/components/tetrons/toolbar/ClipboardGroup.tsx +58 -0
  16. package/dist/components/tetrons/toolbar/FileGroup.tsx +66 -0
  17. package/dist/components/tetrons/toolbar/FontStyleGroup.tsx +194 -0
  18. package/dist/components/tetrons/toolbar/InsertGroup.tsx +267 -0
  19. package/dist/components/tetrons/toolbar/ListAlignGroup.tsx +69 -0
  20. package/dist/components/tetrons/toolbar/MiscGroup.d.ts +7 -0
  21. package/dist/components/tetrons/toolbar/MiscGroup.jsx +55 -0
  22. package/dist/components/tetrons/toolbar/MiscGroup.tsx +104 -0
  23. package/dist/components/tetrons/toolbar/TableContextMenu.tsx +91 -0
  24. package/dist/components/tetrons/toolbar/TetronsToolbar.d.ts +6 -0
  25. package/dist/components/tetrons/toolbar/TetronsToolbar.tsx +71 -0
  26. package/dist/components/tetrons/toolbar/ToolbarButton.tsx +36 -0
  27. package/dist/components/tetrons/toolbar/extensions/Comment.ts +72 -0
  28. package/dist/components/tetrons/toolbar/extensions/Embed.ts +113 -0
  29. package/dist/components/tetrons/toolbar/extensions/FontFamily.ts +43 -0
  30. package/dist/components/tetrons/toolbar/extensions/FontSize.ts +43 -0
  31. package/dist/components/tetrons/toolbar/extensions/ResizableTable.ts +16 -0
  32. package/dist/components/tetrons/toolbar/marks/Subscript.ts +45 -0
  33. package/dist/components/tetrons/toolbar/marks/Superscript.ts +45 -0
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.js +17366 -22
  36. package/dist/index.mjs +4705 -4592
  37. package/dist/styles/styles/tetrons.css +1 -1
  38. package/dist/styles/tetrons.css +1 -1
  39. package/dist/utils/checkGrammar.d.ts +25 -0
  40. package/dist/utils/checkGrammar.js +17 -0
  41. package/package.json +9 -9
@@ -0,0 +1,194 @@
1
+ "use client";
2
+
3
+ import {
4
+ MdFormatBold,
5
+ MdFormatItalic,
6
+ MdFormatUnderlined,
7
+ MdStrikethroughS,
8
+ MdSubscript,
9
+ MdSuperscript,
10
+ MdFormatClear,
11
+ MdFormatPaint,
12
+ } from "react-icons/md";
13
+ import { ImTextColor } from "react-icons/im";
14
+ import { BiSolidColorFill } from "react-icons/bi";
15
+ import { Editor } from "@tiptap/react";
16
+ import ToolbarButton from "./ToolbarButton";
17
+ import React, { useEffect, useState } from "react";
18
+
19
+ interface FontStyleGroupProps {
20
+ editor: Editor;
21
+ }
22
+
23
+ export default function FontStyleGroup({ editor }: FontStyleGroupProps) {
24
+ const [textColor, setTextColor] = useState("#000000");
25
+ const [highlightColor, setHighlightColor] = useState("#ffff00");
26
+ const [fontFamily, setFontFamily] = useState("Arial");
27
+ const [fontSize, setFontSize] = useState("16px");
28
+
29
+ useEffect(() => {
30
+ if (!editor) return;
31
+
32
+ const updateStates = () => {
33
+ const highlight = editor.getAttributes("highlight");
34
+ setHighlightColor(highlight?.color || "#ffff00");
35
+
36
+ const color = editor.getAttributes("textStyle")?.color;
37
+ setTextColor(color || "#000000");
38
+
39
+ const fontAttr = editor.getAttributes("fontFamily")?.font || "Arial";
40
+ setFontFamily(fontAttr);
41
+
42
+ const sizeAttr = editor.getAttributes("fontSize")?.size || "16px";
43
+ setFontSize(sizeAttr);
44
+ };
45
+
46
+ updateStates();
47
+
48
+ editor.on("selectionUpdate", updateStates);
49
+ editor.on("transaction", updateStates);
50
+
51
+ return () => {
52
+ editor.off("selectionUpdate", updateStates);
53
+ editor.off("transaction", updateStates);
54
+ };
55
+ }, [editor]);
56
+
57
+ return (
58
+ <div className="font-style-group">
59
+ <select
60
+ title="Font Family"
61
+ value={fontFamily}
62
+ onChange={(e) => {
63
+ const value = e.target.value;
64
+ setFontFamily(value);
65
+ editor.chain().focus().setFontFamily(value).run();
66
+ }}
67
+ >
68
+ <option value="Arial">Arial</option>
69
+ <option value="Georgia">Georgia</option>
70
+ <option value="Times New Roman">Times New Roman</option>
71
+ <option value="Courier New">Courier New</option>
72
+ <option value="Verdana">Verdana</option>
73
+ </select>
74
+
75
+ <select
76
+ title="Font Size"
77
+ value={fontSize}
78
+ onChange={(e) => {
79
+ const value = e.target.value;
80
+ setFontSize(value);
81
+ editor.chain().focus().setFontSize(value).run();
82
+ }}
83
+ >
84
+ <option value="12px">12</option>
85
+ <option value="14px">14</option>
86
+ <option value="16px">16</option>
87
+ <option value="18px">18</option>
88
+ <option value="24px">24</option>
89
+ <option value="36px">36</option>
90
+ <option value="48px">48</option>
91
+ <option value="64px">64</option>
92
+ <option value="72px">72</option>
93
+ </select>
94
+
95
+ <ToolbarButton
96
+ icon={MdFormatBold}
97
+ label="Bold"
98
+ onClick={() => editor.chain().focus().toggleBold().run()}
99
+ isActive={editor.isActive("bold")}
100
+ />
101
+ <ToolbarButton
102
+ icon={MdFormatItalic}
103
+ label="Italic"
104
+ onClick={() => editor.chain().focus().toggleItalic().run()}
105
+ isActive={editor.isActive("italic")}
106
+ />
107
+ <ToolbarButton
108
+ icon={MdFormatUnderlined}
109
+ label="Underline"
110
+ onClick={() => editor.chain().focus().toggleUnderline().run()}
111
+ isActive={editor.isActive("underline")}
112
+ />
113
+ <ToolbarButton
114
+ icon={MdStrikethroughS}
115
+ label="Strikethrough"
116
+ onClick={() => editor.chain().focus().toggleStrike().run()}
117
+ isActive={editor.isActive("strike")}
118
+ />
119
+ <ToolbarButton
120
+ icon={MdSubscript}
121
+ label="Subscript"
122
+ onClick={() => editor.chain().focus().toggleSubscript().run()}
123
+ isActive={editor.isActive("subscript")}
124
+ />
125
+ <ToolbarButton
126
+ icon={MdSuperscript}
127
+ label="Superscript"
128
+ onClick={() => editor.chain().focus().toggleSuperscript().run()}
129
+ isActive={editor.isActive("superscript")}
130
+ />
131
+
132
+ <label
133
+ title="Font Color"
134
+ aria-label="Font Color"
135
+ className="color-label"
136
+ style={{ "--indicator-color": textColor } as React.CSSProperties}
137
+ >
138
+ <ImTextColor size={20} />
139
+ <div className="color-indicator" />
140
+ <input
141
+ type="color"
142
+ value={textColor}
143
+ onChange={(e) => {
144
+ const color = e.target.value;
145
+ setTextColor(color);
146
+ editor.chain().focus().setColor(color).run();
147
+ }}
148
+ />
149
+ </label>
150
+
151
+ <label
152
+ title="Highlight Color"
153
+ aria-label="Highlight Color"
154
+ className="color-label"
155
+ style={{ "--indicator-color": highlightColor } as React.CSSProperties}
156
+ >
157
+ <BiSolidColorFill size={20} />
158
+ <div className="color-indicator" />
159
+ <input
160
+ type="color"
161
+ value={highlightColor}
162
+ onChange={(e) => {
163
+ const color = e.target.value;
164
+ setHighlightColor(color);
165
+ editor.chain().focus().setHighlight({ color }).run();
166
+ }}
167
+ />
168
+ </label>
169
+
170
+ <ToolbarButton
171
+ icon={MdFormatClear}
172
+ label="Clear Formatting"
173
+ onClick={() => editor.chain().focus().unsetAllMarks().run()}
174
+ />
175
+ <ToolbarButton
176
+ icon={MdFormatPaint}
177
+ label="Apply Painter Format"
178
+ onClick={() => {
179
+ const format = JSON.parse(
180
+ localStorage.getItem("formatPainter") || "{}"
181
+ );
182
+ if (format.color) editor.chain().focus().setColor(format.color).run();
183
+ if (format.backgroundColor) {
184
+ editor
185
+ .chain()
186
+ .focus()
187
+ .setHighlight({ color: format.backgroundColor })
188
+ .run();
189
+ }
190
+ }}
191
+ />
192
+ </div>
193
+ );
194
+ }
@@ -0,0 +1,267 @@
1
+ "use client";
2
+
3
+ import React, { useRef, useState } from "react";
4
+ import {
5
+ MdTableChart,
6
+ MdInsertPhoto,
7
+ MdInsertLink,
8
+ MdInsertComment,
9
+ MdInsertEmoticon,
10
+ MdHorizontalRule,
11
+ MdVideoLibrary,
12
+ MdOutlineOndemandVideo,
13
+ } from "react-icons/md";
14
+ import { Editor } from "@tiptap/react";
15
+ import ToolbarButton from "./ToolbarButton";
16
+ import Picker from "@emoji-mart/react";
17
+
18
+ type Emoji = {
19
+ native: string;
20
+ };
21
+
22
+ export default function InsertGroup({ editor }: { editor: Editor }) {
23
+ const [showTableGrid, setShowTableGrid] = useState(false);
24
+ const [selectedRows, setSelectedRows] = useState(1);
25
+ const [selectedCols, setSelectedCols] = useState(1);
26
+
27
+ const imageInputRef = useRef<HTMLInputElement>(null);
28
+ const videoInputRef = useRef<HTMLInputElement>(null);
29
+
30
+ const [showPicker, setShowPicker] = useState(false);
31
+
32
+ const addEmoji = (emoji: Emoji) => {
33
+ editor.chain().focus().insertContent(emoji.native).run();
34
+ setShowPicker(false);
35
+ };
36
+
37
+ const handleTableCellHover = (row: number, col: number) => {
38
+ setSelectedRows(row);
39
+ setSelectedCols(col);
40
+ };
41
+
42
+ const handleTableInsert = (rows: number, cols: number) => {
43
+ editor
44
+ .chain()
45
+ .focus()
46
+ .insertTable({
47
+ rows: rows || 1,
48
+ cols: cols || 1,
49
+ withHeaderRow: true,
50
+ })
51
+ .run();
52
+
53
+ setShowTableGrid(false);
54
+ setSelectedRows(1);
55
+ setSelectedCols(1);
56
+ };
57
+
58
+ const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
59
+ const file = e.target.files?.[0];
60
+ if (file) {
61
+ const reader = new FileReader();
62
+ reader.onload = () => {
63
+ editor
64
+ .chain()
65
+ .focus()
66
+ .setImage({ src: reader.result as string })
67
+ .run();
68
+ };
69
+ reader.readAsDataURL(file);
70
+ }
71
+ };
72
+
73
+ const handleVideoUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
74
+ const file = e.target.files?.[0];
75
+ if (file) {
76
+ const reader = new FileReader();
77
+ reader.onload = () => {
78
+ editor
79
+ .chain()
80
+ .focus()
81
+ .setVideo({ src: reader.result as string, controls: true })
82
+ .run();
83
+ };
84
+ reader.readAsDataURL(file);
85
+ }
86
+ };
87
+
88
+ function normalizeEmbedUrl(url: string): string {
89
+ try {
90
+ const u = new URL(url);
91
+ const hostname = u.hostname.replace("www.", "").toLowerCase();
92
+ if (hostname === "youtube.com" || hostname === "youtu.be") {
93
+ let videoId = u.searchParams.get("v");
94
+ if (!videoId && hostname === "youtu.be") {
95
+ videoId = u.pathname.slice(1);
96
+ }
97
+ if (videoId) {
98
+ return `https://www.youtube.com/embed/${videoId}`;
99
+ }
100
+ }
101
+ if (hostname === "vimeo.com") {
102
+ const videoId = u.pathname.split("/")[1];
103
+ if (videoId) {
104
+ return `https://player.vimeo.com/video/${videoId}`;
105
+ }
106
+ }
107
+ if (
108
+ hostname === "google.com" ||
109
+ hostname === "maps.google.com" ||
110
+ hostname === "goo.gl"
111
+ ) {
112
+ if (url.includes("/maps/embed")) {
113
+ return url;
114
+ }
115
+ return url.replace("/maps/", "/maps/embed/");
116
+ }
117
+ return url;
118
+ } catch {
119
+ return url;
120
+ }
121
+ }
122
+
123
+ return (
124
+ <div className="insert-group">
125
+ <input
126
+ type="file"
127
+ accept="image/*"
128
+ ref={imageInputRef}
129
+ className="hidden-input"
130
+ onChange={handleImageUpload}
131
+ aria-label="Upload Image"
132
+ title="Upload Image"
133
+ />
134
+ <input
135
+ type="file"
136
+ accept="video/*"
137
+ ref={videoInputRef}
138
+ className="hidden-input"
139
+ onChange={handleVideoUpload}
140
+ aria-label="Upload Video"
141
+ title="Upload Video"
142
+ />
143
+
144
+ <ToolbarButton
145
+ icon={MdTableChart}
146
+ label="Insert Table"
147
+ onClick={() => setShowTableGrid(!showTableGrid)}
148
+ />
149
+ {showTableGrid && (
150
+ <div
151
+ className="table-grid-popup"
152
+ onMouseLeave={() => setShowTableGrid(false)}
153
+ >
154
+ <div className="table-grid">
155
+ {[...Array(10)].map((_, row) =>
156
+ [...Array(10)].map((_, col) => {
157
+ const isSelected = row < selectedRows && col < selectedCols;
158
+ return (
159
+ <div
160
+ key={`${row}-${col}`}
161
+ className={`table-grid-cell ${
162
+ isSelected ? "selected" : ""
163
+ }`}
164
+ onMouseEnter={() => handleTableCellHover(row + 1, col + 1)}
165
+ onClick={() => handleTableInsert(row + 1, col + 1)}
166
+ />
167
+ );
168
+ })
169
+ )}
170
+ </div>
171
+ <div className="table-grid-label">
172
+ {selectedRows} x {selectedCols}
173
+ </div>
174
+ </div>
175
+ )}
176
+
177
+ <ToolbarButton
178
+ icon={MdInsertPhoto}
179
+ label="Insert Image"
180
+ onClick={() => imageInputRef.current?.click()}
181
+ />
182
+ <ToolbarButton
183
+ icon={MdVideoLibrary}
184
+ label="Insert Video"
185
+ onClick={() => videoInputRef.current?.click()}
186
+ />
187
+ <ToolbarButton
188
+ icon={MdInsertLink}
189
+ label="Insert Link"
190
+ onClick={() => {
191
+ const url = prompt("Enter URL");
192
+ if (url) {
193
+ editor
194
+ .chain()
195
+ .focus()
196
+ .extendMarkRange("link")
197
+ .setLink({ href: url })
198
+ .run();
199
+ }
200
+ }}
201
+ />
202
+ <ToolbarButton
203
+ icon={MdInsertComment}
204
+ label="Insert Comment"
205
+ onClick={() => {
206
+ const comment = prompt("Enter your comment");
207
+ if (
208
+ comment &&
209
+ editor.can().chain().focus().setComment(comment).run()
210
+ ) {
211
+ editor.chain().focus().setComment(comment).run();
212
+ } else {
213
+ console.warn("Cannot apply comment — maybe no selection?");
214
+ }
215
+ }}
216
+ />
217
+ <div className="relative">
218
+ <ToolbarButton
219
+ icon={MdInsertEmoticon}
220
+ label="Emoji"
221
+ onClick={() => setShowPicker(!showPicker)}
222
+ />
223
+ {showPicker && (
224
+ <div className="emoji-picker">
225
+ <Picker
226
+ onEmojiSelect={addEmoji}
227
+ theme="auto"
228
+ emoji="point_up"
229
+ showPreview={false}
230
+ showSkinTones={true}
231
+ emojiTooltip={true}
232
+ />
233
+ </div>
234
+ )}
235
+ </div>
236
+ <ToolbarButton
237
+ icon={MdHorizontalRule}
238
+ label="Horizontal Line"
239
+ onClick={() => editor.chain().focus().setHorizontalRule().run()}
240
+ />
241
+ <ToolbarButton
242
+ icon={MdOutlineOndemandVideo}
243
+ label="Embed"
244
+ onClick={() => {
245
+ const url = prompt(
246
+ "Enter embed URL (YouTube, Vimeo, Google Maps, etc.)"
247
+ );
248
+ if (!url) return;
249
+
250
+ const embedUrl = normalizeEmbedUrl(url);
251
+ const width = prompt("Enter width in pixels", "560");
252
+ const height = prompt("Enter height in pixels", "315");
253
+
254
+ editor
255
+ .chain()
256
+ .focus()
257
+ .setEmbed({
258
+ src: embedUrl,
259
+ width: width ? parseInt(width) : 560,
260
+ height: height ? parseInt(height) : 315,
261
+ })
262
+ .run();
263
+ }}
264
+ />
265
+ </div>
266
+ );
267
+ }
@@ -0,0 +1,69 @@
1
+ "use client";
2
+ import React from "react";
3
+ import {
4
+ MdFormatListBulleted,
5
+ MdFormatListNumbered,
6
+ MdFormatIndentDecrease,
7
+ MdFormatIndentIncrease,
8
+ MdFormatAlignLeft,
9
+ MdFormatAlignCenter,
10
+ MdFormatAlignRight,
11
+ MdFormatAlignJustify,
12
+ } from "react-icons/md";
13
+ import { Editor } from "@tiptap/react";
14
+ import ToolbarButton from "./ToolbarButton";
15
+
16
+ export default function ListAlignGroup({ editor }: { editor: Editor }) {
17
+ return (
18
+ <div className="list-align-group">
19
+ <ToolbarButton
20
+ icon={MdFormatListBulleted}
21
+ title="Bulleted List"
22
+ onClick={() => editor.chain().focus().toggleBulletList().run()}
23
+ disabled={!editor.can().toggleBulletList()}
24
+ />
25
+ <ToolbarButton
26
+ icon={MdFormatListNumbered}
27
+ title="Numbered List"
28
+ onClick={() => editor.chain().focus().toggleOrderedList().run()}
29
+ disabled={!editor.can().toggleOrderedList()}
30
+ />
31
+ <ToolbarButton
32
+ icon={MdFormatIndentIncrease}
33
+ title="Increase Indent"
34
+ onClick={() => editor.chain().focus().sinkListItem("listItem").run()}
35
+ disabled={!editor.can().sinkListItem("listItem")}
36
+ />
37
+ <ToolbarButton
38
+ icon={MdFormatIndentDecrease}
39
+ title="Decrease Indent"
40
+ onClick={() => editor.chain().focus().liftListItem("listItem").run()}
41
+ disabled={!editor.can().liftListItem("listItem")}
42
+ />
43
+ <ToolbarButton
44
+ icon={MdFormatAlignLeft}
45
+ title="Align Left"
46
+ onClick={() => editor.chain().focus().setTextAlign("left").run()}
47
+ disabled={!editor.can().setTextAlign("left")}
48
+ />
49
+ <ToolbarButton
50
+ icon={MdFormatAlignCenter}
51
+ title="Align Center"
52
+ onClick={() => editor.chain().focus().setTextAlign("center").run()}
53
+ disabled={!editor.can().setTextAlign("center")}
54
+ />
55
+ <ToolbarButton
56
+ icon={MdFormatAlignRight}
57
+ title="Align Right"
58
+ onClick={() => editor.chain().focus().setTextAlign("right").run()}
59
+ disabled={!editor.can().setTextAlign("right")}
60
+ />
61
+ <ToolbarButton
62
+ icon={MdFormatAlignJustify}
63
+ title="Justify"
64
+ onClick={() => editor.chain().focus().setTextAlign("justify").run()}
65
+ disabled={!editor.can().setTextAlign("justify")}
66
+ />
67
+ </div>
68
+ );
69
+ }
@@ -0,0 +1,7 @@
1
+ import React from "react";
2
+ import { Editor } from "@tiptap/react";
3
+ interface MiscGroupProps {
4
+ editor: Editor;
5
+ }
6
+ export default function MiscGroup({ editor }: MiscGroupProps): React.JSX.Element;
7
+ export {};
@@ -0,0 +1,55 @@
1
+ import React from "react";
2
+ import { MdUndo, MdRedo, MdRefresh, MdVisibility, MdCode, MdSpellcheck, } from "react-icons/md";
3
+ import ToolbarButton from "./ToolbarButton";
4
+ import { checkGrammar } from "../../../utils/checkGrammar";
5
+ export default function MiscGroup({ editor }) {
6
+ const handlePreview = () => {
7
+ const html = editor.getHTML();
8
+ const previewWindow = window.open("", "_blank");
9
+ if (previewWindow) {
10
+ previewWindow.document.open();
11
+ previewWindow.document.write(`
12
+ <html>
13
+ <head>
14
+ <title>Preview</title>
15
+ <style>
16
+ body { font-family: sans-serif; padding: 2rem; line-height: 1.6; }
17
+ </style>
18
+ </head>
19
+ <body>${html}</body>
20
+ </html>
21
+ `);
22
+ previewWindow.document.close();
23
+ }
24
+ };
25
+ const handleGrammarCheck = async () => {
26
+ const text = editor.getText();
27
+ try {
28
+ const issues = await checkGrammar(text);
29
+ if (issues.length === 0) {
30
+ alert("āœ… No grammar issues found.");
31
+ return;
32
+ }
33
+ let message = "šŸ”Ž Grammar Suggestions:\n\n";
34
+ issues.forEach((issue, idx) => {
35
+ const replacements = issue.replacements
36
+ .map((r) => r.value)
37
+ .join(", ");
38
+ message += `${idx + 1}. "${issue.context.text}"\n→ ${replacements}\nReason: ${issue.message}\n\n`;
39
+ });
40
+ alert(message);
41
+ }
42
+ catch (err) {
43
+ console.error(err);
44
+ alert("āŒ Failed to check grammar. Please try again later.");
45
+ }
46
+ };
47
+ return (<div className="misc-group">
48
+ <ToolbarButton icon={MdUndo} label="Undo" onClick={() => editor.chain().focus().undo().run()} disabled={!editor.can().undo()}/>
49
+ <ToolbarButton icon={MdRedo} label="Redo" onClick={() => editor.chain().focus().redo().run()} disabled={!editor.can().redo()}/>
50
+ <ToolbarButton icon={MdRefresh} label="Reset Formatting" onClick={() => editor.chain().focus().unsetAllMarks().clearNodes().run()}/>
51
+ <ToolbarButton icon={MdCode} label="Toggle Code Block" onClick={() => editor.chain().focus().toggleCodeBlock().run()} isActive={editor.isActive("codeBlock")}/>
52
+ <ToolbarButton icon={MdVisibility} label="Preview" onClick={handlePreview}/>
53
+ <ToolbarButton icon={MdSpellcheck} label="Check Grammar" onClick={handleGrammarCheck}/>
54
+ </div>);
55
+ }
@@ -0,0 +1,104 @@
1
+ import React from "react";
2
+ import {
3
+ MdUndo,
4
+ MdRedo,
5
+ MdRefresh,
6
+ MdVisibility,
7
+ MdCode,
8
+ MdSpellcheck,
9
+ } from "react-icons/md";
10
+ import { Editor } from "@tiptap/react";
11
+ import ToolbarButton from "./ToolbarButton";
12
+ import { checkGrammar, GrammarMatch } from "../../../utils/checkGrammar";
13
+
14
+ interface MiscGroupProps {
15
+ editor: Editor;
16
+ }
17
+
18
+ export default function MiscGroup({ editor }: MiscGroupProps) {
19
+ const handlePreview = () => {
20
+ const html = editor.getHTML();
21
+ const previewWindow = window.open("", "_blank");
22
+ if (previewWindow) {
23
+ previewWindow.document.open();
24
+ previewWindow.document.write(`
25
+ <html>
26
+ <head>
27
+ <title>Preview</title>
28
+ <style>
29
+ body { font-family: sans-serif; padding: 2rem; line-height: 1.6; }
30
+ </style>
31
+ </head>
32
+ <body>${html}</body>
33
+ </html>
34
+ `);
35
+ previewWindow.document.close();
36
+ }
37
+ };
38
+
39
+ const handleGrammarCheck = async () => {
40
+ const text = editor.getText();
41
+ try {
42
+ const issues: GrammarMatch[] = await checkGrammar(text);
43
+ if (issues.length === 0) {
44
+ alert("āœ… No grammar issues found.");
45
+ return;
46
+ }
47
+
48
+ let message = "šŸ”Ž Grammar Suggestions:\n\n";
49
+ issues.forEach((issue: GrammarMatch, idx: number) => {
50
+ const replacements = issue.replacements
51
+ .map((r: { value: string }) => r.value)
52
+ .join(", ");
53
+ message += `${idx + 1}. "${
54
+ issue.context.text
55
+ }"\n→ ${replacements}\nReason: ${issue.message}\n\n`;
56
+ });
57
+
58
+ alert(message);
59
+ } catch (err) {
60
+ console.error(err);
61
+ alert("āŒ Failed to check grammar. Please try again later.");
62
+ }
63
+ };
64
+
65
+ return (
66
+ <div className="misc-group">
67
+ <ToolbarButton
68
+ icon={MdUndo}
69
+ label="Undo"
70
+ onClick={() => editor.chain().focus().undo().run()}
71
+ disabled={!editor.can().undo()}
72
+ />
73
+ <ToolbarButton
74
+ icon={MdRedo}
75
+ label="Redo"
76
+ onClick={() => editor.chain().focus().redo().run()}
77
+ disabled={!editor.can().redo()}
78
+ />
79
+ <ToolbarButton
80
+ icon={MdRefresh}
81
+ label="Reset Formatting"
82
+ onClick={() =>
83
+ editor.chain().focus().unsetAllMarks().clearNodes().run()
84
+ }
85
+ />
86
+ <ToolbarButton
87
+ icon={MdCode}
88
+ label="Toggle Code Block"
89
+ onClick={() => editor.chain().focus().toggleCodeBlock().run()}
90
+ isActive={editor.isActive("codeBlock")}
91
+ />
92
+ <ToolbarButton
93
+ icon={MdVisibility}
94
+ label="Preview"
95
+ onClick={handlePreview}
96
+ />
97
+ <ToolbarButton
98
+ icon={MdSpellcheck}
99
+ label="Check Grammar"
100
+ onClick={handleGrammarCheck}
101
+ />
102
+ </div>
103
+ );
104
+ }