tetrons 2.3.26 → 2.3.28

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 (51) hide show
  1. package/dist/app/api/ai-action/route.d.mts +9 -0
  2. package/dist/app/api/ai-action/route.mjs +36 -0
  3. package/dist/app/api/export/route.d.mts +3 -0
  4. package/dist/app/api/export/route.mjs +8 -0
  5. package/dist/app/api/register/route.d.mts +10 -0
  6. package/dist/app/api/register/route.mjs +85 -0
  7. package/dist/app/api/save/route.d.mts +9 -0
  8. package/dist/app/api/save/route.mjs +18 -0
  9. package/dist/app/api/transcribe/route.d.mts +10 -0
  10. package/dist/app/api/transcribe/route.mjs +46 -0
  11. package/dist/app/api/validate/route.d.mts +13 -0
  12. package/dist/app/api/validate/route.mjs +107 -0
  13. package/dist/components/tetrons/ResizableImageComponent.tsx +64 -29
  14. package/dist/index.mjs +93 -56
  15. package/package.json +2 -2
  16. package/dist/app/page.d.ts +0 -2
  17. package/dist/components/components/UI/Button.tsx +0 -0
  18. package/dist/components/components/UI/Dropdown.tsx +0 -0
  19. package/dist/components/components/tetrons/EditorContent.tsx +0 -280
  20. package/dist/components/components/tetrons/ResizableImageComponent.tsx +0 -77
  21. package/dist/components/components/tetrons/ResizableVideoComponent.tsx +0 -56
  22. package/dist/components/tetrons/EditorContent.d.ts +0 -6
  23. package/dist/components/tetrons/ResizableImage.d.ts +0 -1
  24. package/dist/components/tetrons/ResizableImage.js +0 -36
  25. package/dist/components/tetrons/ResizableImage.ts +0 -39
  26. package/dist/components/tetrons/ResizableImageComponent.d.ts +0 -4
  27. package/dist/components/tetrons/ResizableImageComponent.jsx +0 -37
  28. package/dist/components/tetrons/ResizableVideo.ts +0 -66
  29. package/dist/components/tetrons/extensions/Spellcheck.ts +0 -50
  30. package/dist/components/tetrons/helpers.ts +0 -0
  31. package/dist/components/tetrons/toolbar/AIGroup.tsx +0 -209
  32. package/dist/components/tetrons/toolbar/ActionGroup.tsx +0 -218
  33. package/dist/components/tetrons/toolbar/ClipboardGroup.tsx +0 -58
  34. package/dist/components/tetrons/toolbar/FileGroup.tsx +0 -66
  35. package/dist/components/tetrons/toolbar/FontStyleGroup.tsx +0 -194
  36. package/dist/components/tetrons/toolbar/InsertGroup.tsx +0 -267
  37. package/dist/components/tetrons/toolbar/ListAlignGroup.tsx +0 -69
  38. package/dist/components/tetrons/toolbar/MiscGroup.tsx +0 -104
  39. package/dist/components/tetrons/toolbar/TableContextMenu.tsx +0 -91
  40. package/dist/components/tetrons/toolbar/TetronsToolbar.tsx +0 -77
  41. package/dist/components/tetrons/toolbar/ToolbarButton.tsx +0 -36
  42. package/dist/components/tetrons/toolbar/extensions/Comment.ts +0 -72
  43. package/dist/components/tetrons/toolbar/extensions/Embed.ts +0 -113
  44. package/dist/components/tetrons/toolbar/extensions/FontFamily.ts +0 -43
  45. package/dist/components/tetrons/toolbar/extensions/FontSize.ts +0 -43
  46. package/dist/components/tetrons/toolbar/extensions/ResizableTable.ts +0 -16
  47. package/dist/components/tetrons/toolbar/marks/Subscript.ts +0 -45
  48. package/dist/components/tetrons/toolbar/marks/Superscript.ts +0 -45
  49. package/dist/index.d.ts +0 -6
  50. package/dist/index.js +0 -16975
  51. package/dist/styles/styles/tetrons.css +0 -563
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/components/tetrons/EditorContent.tsx
2
- import React14, { useEffect as useEffect8, useRef as useRef7, useState as useState8 } from "react";
2
+ import React14, { useEffect as useEffect8, useRef as useRef7, useState as useState9 } from "react";
3
3
  import {
4
4
  useEditor,
5
5
  EditorContent as TiptapEditorContent
@@ -15364,7 +15364,7 @@ import Image from "@tiptap/extension-image";
15364
15364
  import { ReactNodeViewRenderer } from "@tiptap/react";
15365
15365
 
15366
15366
  // src/components/tetrons/ResizableImageComponent.tsx
15367
- import React, { useRef, useEffect as useEffect2 } from "react";
15367
+ import React, { useRef, useEffect as useEffect2, useState as useState2 } from "react";
15368
15368
  import { NodeViewWrapper } from "@tiptap/react";
15369
15369
  var ResizableImageComponent = ({
15370
15370
  node,
@@ -15372,19 +15372,36 @@ var ResizableImageComponent = ({
15372
15372
  selected
15373
15373
  }) => {
15374
15374
  const { src, alt, title, width, height } = node.attrs;
15375
+ const defaultWidth = width ?? 300;
15376
+ const defaultHeight = height ?? 200;
15377
+ const aspectRatio = defaultHeight > 0 ? defaultWidth / defaultHeight : 4 / 3;
15375
15378
  const wrapperRef = useRef(null);
15376
- const imgRef = useRef(null);
15379
+ const [isResizing, setIsResizing] = useState2(false);
15377
15380
  useEffect2(() => {
15378
- const img = imgRef.current;
15379
- if (!img) return;
15380
- const observer = new ResizeObserver(() => {
15381
- const w = Math.round(img.offsetWidth);
15382
- const h = Math.round(img.offsetHeight);
15383
- updateAttributes({ width: w, height: h });
15384
- });
15385
- observer.observe(img);
15386
- return () => observer.disconnect();
15387
- }, [updateAttributes]);
15381
+ const handleMouseMove2 = (e) => {
15382
+ if (!isResizing || !wrapperRef.current) return;
15383
+ const rect = wrapperRef.current.getBoundingClientRect();
15384
+ const newWidth = e.clientX - rect.left;
15385
+ const newHeight = newWidth / aspectRatio;
15386
+ if (newWidth > 50 && newHeight > 50) {
15387
+ updateAttributes({
15388
+ width: Math.round(newWidth),
15389
+ height: Math.round(newHeight)
15390
+ });
15391
+ }
15392
+ };
15393
+ const handleMouseUp = () => {
15394
+ setIsResizing(false);
15395
+ };
15396
+ if (isResizing) {
15397
+ window.addEventListener("mousemove", handleMouseMove2);
15398
+ window.addEventListener("mouseup", handleMouseUp);
15399
+ }
15400
+ return () => {
15401
+ window.removeEventListener("mousemove", handleMouseMove2);
15402
+ window.removeEventListener("mouseup", handleMouseUp);
15403
+ };
15404
+ }, [isResizing, updateAttributes, aspectRatio]);
15388
15405
  return /* @__PURE__ */ React.createElement(
15389
15406
  NodeViewWrapper,
15390
15407
  {
@@ -15392,31 +15409,55 @@ var ResizableImageComponent = ({
15392
15409
  contentEditable: false,
15393
15410
  className: `resizable-image-wrapper ${selected ? "ProseMirror-selectednode" : ""}`,
15394
15411
  style: {
15395
- resize: "both",
15396
- overflow: "auto",
15412
+ position: "relative",
15397
15413
  border: "1px solid #ccc",
15398
- padding: 2,
15399
15414
  display: "inline-block",
15400
- maxWidth: "100%"
15415
+ width: `${defaultWidth}px`,
15416
+ height: `${defaultHeight}px`,
15417
+ minWidth: "50px",
15418
+ minHeight: "50px",
15419
+ maxWidth: "100%",
15420
+ userSelect: "none",
15421
+ padding: 2
15401
15422
  }
15402
15423
  },
15403
15424
  /* @__PURE__ */ React.createElement(
15404
15425
  "img",
15405
15426
  {
15406
- ref: imgRef,
15407
15427
  src,
15408
15428
  alt: alt ?? "",
15409
15429
  title: title ?? "",
15410
15430
  loading: "lazy",
15411
15431
  style: {
15412
- width: width ? `${width}px` : "auto",
15413
- height: height ? `${height}px` : "auto",
15432
+ width: "100%",
15433
+ height: "100%",
15434
+ objectFit: "contain",
15414
15435
  display: "block",
15415
15436
  userSelect: "none",
15416
15437
  pointerEvents: "auto"
15417
15438
  },
15418
15439
  draggable: false
15419
15440
  }
15441
+ ),
15442
+ /* @__PURE__ */ React.createElement(
15443
+ "div",
15444
+ {
15445
+ onMouseDown: (e) => {
15446
+ e.preventDefault();
15447
+ setIsResizing(true);
15448
+ },
15449
+ style: {
15450
+ position: "absolute",
15451
+ width: "12px",
15452
+ height: "12px",
15453
+ right: 2,
15454
+ bottom: 2,
15455
+ background: "#ccc",
15456
+ borderRadius: "2px",
15457
+ cursor: "nwse-resize",
15458
+ zIndex: 10
15459
+ }
15460
+ }
15420
15461
  )
15421
15462
  );
15422
15463
  };
@@ -15428,12 +15469,8 @@ var ResizableImage = Image.extend({
15428
15469
  addAttributes() {
15429
15470
  return {
15430
15471
  ...this.parent?.(),
15431
- width: {
15432
- default: null
15433
- },
15434
- height: {
15435
- default: null
15436
- }
15472
+ width: { default: null },
15473
+ height: { default: null }
15437
15474
  };
15438
15475
  },
15439
15476
  renderHTML({ HTMLAttributes }) {
@@ -15558,9 +15595,9 @@ var ResizableVideo = Node13.create({
15558
15595
  });
15559
15596
 
15560
15597
  // src/components/tetrons/toolbar/TableContextMenu.tsx
15561
- import React3, { useEffect as useEffect4, useState as useState2 } from "react";
15598
+ import React3, { useEffect as useEffect4, useState as useState3 } from "react";
15562
15599
  function TableContextMenu({ editor }) {
15563
- const [menuPosition, setMenuPosition] = useState2(null);
15600
+ const [menuPosition, setMenuPosition] = useState3(null);
15564
15601
  useEffect4(() => {
15565
15602
  const handleContextMenu = (event) => {
15566
15603
  const target = event.target;
@@ -15644,10 +15681,10 @@ function TableContextMenu({ editor }) {
15644
15681
  }
15645
15682
 
15646
15683
  // src/components/tetrons/toolbar/TetronsToolbar.tsx
15647
- import React13, { useEffect as useEffect7, useState as useState7 } from "react";
15684
+ import React13, { useEffect as useEffect7, useState as useState8 } from "react";
15648
15685
 
15649
15686
  // src/components/tetrons/toolbar/ActionGroup.tsx
15650
- import React5, { useEffect as useEffect5, useRef as useRef3, useState as useState3 } from "react";
15687
+ import React5, { useEffect as useEffect5, useRef as useRef3, useState as useState4 } from "react";
15651
15688
  import {
15652
15689
  MdZoomIn,
15653
15690
  MdZoomOut,
@@ -15680,7 +15717,7 @@ var ToolbarButton_default = ToolbarButton;
15680
15717
 
15681
15718
  // src/components/tetrons/toolbar/ActionGroup.tsx
15682
15719
  function ActionGroup({ editor }) {
15683
- const [dropdownOpen, setDropdownOpen] = useState3(false);
15720
+ const [dropdownOpen, setDropdownOpen] = useState4(false);
15684
15721
  const dropdownRef = useRef3(null);
15685
15722
  useEffect5(() => {
15686
15723
  const handleClickOutside = (event) => {
@@ -15925,12 +15962,12 @@ import {
15925
15962
  } from "react-icons/md";
15926
15963
  import { ImTextColor } from "react-icons/im";
15927
15964
  import { BiSolidColorFill } from "react-icons/bi";
15928
- import React7, { useEffect as useEffect6, useState as useState4 } from "react";
15965
+ import React7, { useEffect as useEffect6, useState as useState5 } from "react";
15929
15966
  function FontStyleGroup({ editor }) {
15930
- const [textColor, setTextColor] = useState4("#000000");
15931
- const [highlightColor, setHighlightColor] = useState4("#ffff00");
15932
- const [fontFamily, setFontFamily] = useState4("Arial");
15933
- const [fontSize, setFontSize] = useState4("16px");
15967
+ const [textColor, setTextColor] = useState5("#000000");
15968
+ const [highlightColor, setHighlightColor] = useState5("#ffff00");
15969
+ const [fontFamily, setFontFamily] = useState5("Arial");
15970
+ const [fontSize, setFontSize] = useState5("16px");
15934
15971
  useEffect6(() => {
15935
15972
  if (!editor) return;
15936
15973
  const updateStates = () => {
@@ -16105,7 +16142,7 @@ function FontStyleGroup({ editor }) {
16105
16142
  }
16106
16143
 
16107
16144
  // src/components/tetrons/toolbar/InsertGroup.tsx
16108
- import React8, { useRef as useRef4, useState as useState5 } from "react";
16145
+ import React8, { useRef as useRef4, useState as useState6 } from "react";
16109
16146
  import {
16110
16147
  MdTableChart,
16111
16148
  MdInsertPhoto,
@@ -16118,12 +16155,12 @@ import {
16118
16155
  } from "react-icons/md";
16119
16156
  import Picker from "@emoji-mart/react";
16120
16157
  function InsertGroup({ editor }) {
16121
- const [showTableGrid, setShowTableGrid] = useState5(false);
16122
- const [selectedRows, setSelectedRows] = useState5(1);
16123
- const [selectedCols, setSelectedCols] = useState5(1);
16158
+ const [showTableGrid, setShowTableGrid] = useState6(false);
16159
+ const [selectedRows, setSelectedRows] = useState6(1);
16160
+ const [selectedCols, setSelectedCols] = useState6(1);
16124
16161
  const imageInputRef = useRef4(null);
16125
16162
  const videoInputRef = useRef4(null);
16126
- const [showPicker, setShowPicker] = useState5(false);
16163
+ const [showPicker, setShowPicker] = useState6(false);
16127
16164
  const addEmoji = (emoji) => {
16128
16165
  editor.chain().focus().insertContent(emoji.native).run();
16129
16166
  setShowPicker(false);
@@ -16584,19 +16621,19 @@ function FileGroup({ editor }) {
16584
16621
  }
16585
16622
 
16586
16623
  // src/components/tetrons/toolbar/AIGroup.tsx
16587
- import React12, { useState as useState6, useRef as useRef6 } from "react";
16624
+ import React12, { useState as useState7, useRef as useRef6 } from "react";
16588
16625
  import { FaMicrophone, FaStop } from "react-icons/fa";
16589
16626
  import { Waveform } from "@uiball/loaders";
16590
16627
  import { motion, AnimatePresence } from "framer-motion";
16591
16628
  function AiGroup({ editor }) {
16592
- const [isRecording, setIsRecording] = useState6(false);
16593
- const [audioBlob, setAudioBlob] = useState6(null);
16594
- const [isTranscribing, setIsTranscribing] = useState6(false);
16595
- const [transcriptionError, setTranscriptionError] = useState6("");
16596
- const [showPromptInput, setShowPromptInput] = useState6(false);
16597
- const [prompt2, setPrompt] = useState6("");
16598
- const [isLoadingAI, setIsLoadingAI] = useState6(false);
16599
- const [aiError, setAiError] = useState6("");
16629
+ const [isRecording, setIsRecording] = useState7(false);
16630
+ const [audioBlob, setAudioBlob] = useState7(null);
16631
+ const [isTranscribing, setIsTranscribing] = useState7(false);
16632
+ const [transcriptionError, setTranscriptionError] = useState7("");
16633
+ const [showPromptInput, setShowPromptInput] = useState7(false);
16634
+ const [prompt2, setPrompt] = useState7("");
16635
+ const [isLoadingAI, setIsLoadingAI] = useState7(false);
16636
+ const [aiError, setAiError] = useState7("");
16600
16637
  const mediaRecorderRef = useRef6(null);
16601
16638
  const chunksRef = useRef6([]);
16602
16639
  const startRecording = async () => {
@@ -16750,7 +16787,7 @@ function TetronsToolbar({
16750
16787
  editor,
16751
16788
  version
16752
16789
  }) {
16753
- const [autoSave, setAutoSave] = useState7(false);
16790
+ const [autoSave, setAutoSave] = useState8(false);
16754
16791
  useEffect7(() => {
16755
16792
  if (!editor) return;
16756
16793
  const handleUpdate = () => {
@@ -16784,11 +16821,11 @@ lowlight.register("js", javascript);
16784
16821
  lowlight.register("ts", typescript);
16785
16822
  function EditorContent({ apiKey }) {
16786
16823
  const typo = useTypo();
16787
- const [isValid, setIsValid] = useState8(null);
16788
- const [error, setError] = useState8(null);
16789
- const [versions, setVersions] = useState8([]);
16790
- const [userVersion, setUserVersion] = useState8(null);
16791
- const [currentVersionIndex, setCurrentVersionIndex] = useState8(
16824
+ const [isValid, setIsValid] = useState9(null);
16825
+ const [error, setError] = useState9(null);
16826
+ const [versions, setVersions] = useState9([]);
16827
+ const [userVersion, setUserVersion] = useState9(null);
16828
+ const [currentVersionIndex, setCurrentVersionIndex] = useState9(
16792
16829
  null
16793
16830
  );
16794
16831
  const wrapperRef = useRef7(null);
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "tetrons",
3
- "version": "2.3.26",
3
+ "version": "2.3.28",
4
4
  "description": "A Next.js project written in TypeScript",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
8
  "dev": "next dev --turbo",
9
- "build": "tsup src/index.ts --format esm,cjs --dts --out-dir dist && copyfiles -u 1 src/styles/*.css dist/styles && copyfiles -u 1 src/components/**/*.tsx dist/components",
9
+ "build": "tsup && copyfiles -u 2 src/styles/*.css dist/styles && copyfiles -u 2 src/components/**/*.tsx dist/components",
10
10
  "start": "next start",
11
11
  "lint": "next lint"
12
12
  },
@@ -1,2 +0,0 @@
1
- import "../styles/tetrons.css";
2
- export default function Home(): import("react").JSX.Element;
File without changes
File without changes
@@ -1,280 +0,0 @@
1
- "use client";
2
-
3
- import React, { useEffect, useRef, useState } from "react";
4
- import {
5
- useEditor,
6
- EditorContent as TiptapEditorContent,
7
- } from "@tiptap/react";
8
-
9
- import Document from "@tiptap/extension-document";
10
- import Paragraph from "@tiptap/extension-paragraph";
11
- import Text from "@tiptap/extension-text";
12
- import History from "@tiptap/extension-history";
13
- import Bold from "@tiptap/extension-bold";
14
- import Italic from "@tiptap/extension-italic";
15
- import Underline from "@tiptap/extension-underline";
16
- import Strike from "@tiptap/extension-strike";
17
- import Code from "@tiptap/extension-code";
18
- import Blockquote from "@tiptap/extension-blockquote";
19
- import HardBreak from "@tiptap/extension-hard-break";
20
- import Heading from "@tiptap/extension-heading";
21
- import HorizontalRule from "@tiptap/extension-horizontal-rule";
22
- import TextAlign from "@tiptap/extension-text-align";
23
- import Color from "@tiptap/extension-color";
24
- import Highlight from "@tiptap/extension-highlight";
25
- import Image from "@tiptap/extension-image";
26
- import Link from "@tiptap/extension-link";
27
- import TextStyle from "@tiptap/extension-text-style";
28
- import ListItem from "@tiptap/extension-list-item";
29
- import BulletList from "@tiptap/extension-bullet-list";
30
- import OrderedList from "@tiptap/extension-ordered-list";
31
- import TableRow from "@tiptap/extension-table-row";
32
- import TableCell from "@tiptap/extension-table-cell";
33
- import TableHeader from "@tiptap/extension-table-header";
34
- import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
35
-
36
- import js from "highlight.js/lib/languages/javascript";
37
- import ts from "highlight.js/lib/languages/typescript";
38
- import { createLowlight } from "lowlight";
39
-
40
- import { useTypo } from "../../utils/useTypo";
41
- import { Spellcheck } from "./extensions/Spellcheck";
42
-
43
- import { Comment } from "./toolbar/extensions/Comment";
44
- import { Subscript } from "./toolbar/marks/Subscript";
45
- import { Superscript } from "./toolbar/marks/Superscript";
46
- import { ResizableTable } from "./toolbar/extensions/ResizableTable";
47
- import { Embed } from "./toolbar/extensions/Embed";
48
- import { FontFamily } from "./toolbar/extensions/FontFamily";
49
- import { FontSize } from "./toolbar/extensions/FontSize";
50
- import { ResizableImage } from "./ResizableImage";
51
- import { ResizableVideo } from "./ResizableVideo";
52
- import TableContextMenu from "./toolbar/TableContextMenu";
53
- import TetronsToolbar from "./toolbar/TetronsToolbar";
54
-
55
- const lowlight = createLowlight();
56
- lowlight.register("js", js);
57
- lowlight.register("ts", ts);
58
-
59
- type EditorContentProps = {
60
- apiKey: string;
61
- };
62
-
63
- export default function EditorContent({ apiKey }: EditorContentProps) {
64
- const typo = useTypo();
65
-
66
- const [isValid, setIsValid] = useState<boolean | null>(null);
67
- const [error, setError] = useState<string | null>(null);
68
- const [versions, setVersions] = useState<string[]>([]);
69
- const [userVersion, setUserVersion] = useState<
70
- "free" | "pro" | "premium" | "platinum" | null
71
- >(null);
72
- const [currentVersionIndex, setCurrentVersionIndex] = useState<number | null>(
73
- null
74
- );
75
-
76
- const wrapperRef = useRef<HTMLDivElement>(null);
77
-
78
- function getApiBaseUrl(): string {
79
- if (
80
- typeof import.meta !== "undefined" &&
81
- import.meta.env?.VITE_TETRONS_API_URL
82
- ) {
83
- return import.meta.env.VITE_TETRONS_API_URL;
84
- }
85
- if (
86
- typeof process !== "undefined" &&
87
- process.env?.NEXT_PUBLIC_TETRONS_API_URL
88
- ) {
89
- return process.env.NEXT_PUBLIC_TETRONS_API_URL;
90
- }
91
- return "https://staging.tetrons.com";
92
- }
93
-
94
- const API_BASE_URL = getApiBaseUrl();
95
-
96
- useEffect(() => {
97
- const validateKey = async () => {
98
- try {
99
- const res = await fetch(`${API_BASE_URL}/api/validate`, {
100
- method: "POST",
101
- headers: {
102
- "Content-Type": "application/json",
103
- },
104
- body: JSON.stringify({ apiKey }),
105
- });
106
-
107
- const data = await res.json();
108
- if (!res.ok) throw new Error(data.error || "Invalid API Key");
109
-
110
- setIsValid(true);
111
- setUserVersion(data.version);
112
- } catch (err: unknown) {
113
- setError(err instanceof Error ? err.message : "Invalid API Key");
114
- setIsValid(false);
115
- }
116
- };
117
-
118
- validateKey();
119
- }, [apiKey, API_BASE_URL]);
120
-
121
- const editor = useEditor({
122
- extensions: [
123
- Document,
124
- Paragraph,
125
- Text,
126
- History,
127
- Bold,
128
- Italic,
129
- Underline,
130
- Strike,
131
- Code,
132
- Blockquote,
133
- HardBreak,
134
- Heading.configure({ levels: [1, 2, 3, 4, 5, 6] }),
135
- HorizontalRule,
136
- TextStyle,
137
- Color,
138
- Highlight.configure({ multicolor: true }),
139
- FontFamily,
140
- FontSize,
141
- TextAlign.configure({ types: ["heading", "paragraph"] }),
142
- ListItem,
143
- BulletList,
144
- OrderedList,
145
- Subscript,
146
- Superscript,
147
- Image,
148
- Link.configure({
149
- openOnClick: false,
150
- autolink: true,
151
- linkOnPaste: true,
152
- }),
153
- ResizableTable.configure({ resizable: true }),
154
- TableRow,
155
- TableCell,
156
- TableHeader,
157
- Embed,
158
- ResizableImage,
159
- ResizableVideo,
160
- Comment,
161
- CodeBlockLowlight.configure({
162
- lowlight,
163
- HTMLAttributes: {
164
- class: "bg-gray-100 p-2 rounded font-mono text-sm overflow-auto",
165
- },
166
- }),
167
- ...(typo
168
- ? [
169
- Spellcheck.configure({
170
- spellcheckFn: (word: string) => typo.check(word),
171
- }),
172
- ]
173
- : []),
174
- ],
175
- content: "",
176
- editorProps: {
177
- attributes: {
178
- class: "min-h-full focus:outline-none p-0",
179
- "data-placeholder": "Start typing here...",
180
- },
181
- },
182
- immediatelyRender: false,
183
- });
184
-
185
- useEffect(() => {
186
- return () => {
187
- editor?.destroy();
188
- };
189
- }, [editor]);
190
-
191
- const handleEditorClick = () => {
192
- if (editor && !editor.isFocused) {
193
- editor.commands.focus();
194
- }
195
- };
196
-
197
- const saveVersion = () => {
198
- if (!editor) return;
199
- const content = editor.getJSON();
200
- setVersions((prev) => [...prev, JSON.stringify(content)]);
201
- setCurrentVersionIndex(versions.length);
202
- };
203
-
204
- const restoreVersion = (index: number) => {
205
- if (!editor) return;
206
- const versionContent = versions[index];
207
- if (versionContent) {
208
- editor.commands.setContent(JSON.parse(versionContent));
209
- setCurrentVersionIndex(index);
210
- }
211
- };
212
-
213
- if (isValid === false) {
214
- return <div className="editor-error">⚠️ {error}</div>;
215
- }
216
-
217
- if (isValid === null) {
218
- return <div className="editor-loading">🔍 Validating license...</div>;
219
- }
220
-
221
- if (!typo) {
222
- return <div className="editor-loading">📖 Loading dictionary...</div>;
223
- }
224
-
225
- return (
226
- <div className="editor-container">
227
- {userVersion !== "free" && (
228
- <div className="editor-toolbar">
229
- <button
230
- type="button"
231
- onClick={saveVersion}
232
- disabled={!editor}
233
- className="editor-save-btn"
234
- >
235
- Save Version
236
- </button>
237
-
238
- <div className="editor-versions-wrapper">
239
- {versions.length === 0 ? (
240
- <span className="editor-no-versions">No saved versions</span>
241
- ) : (
242
- versions.map((_, idx) => (
243
- <button
244
- key={idx}
245
- type="button"
246
- onClick={() => restoreVersion(idx)}
247
- className={`editor-version-btn ${
248
- idx === currentVersionIndex ? "active" : ""
249
- }`}
250
- title={`Restore Version ${idx + 1}`}
251
- >
252
- {`V${idx + 1}`}
253
- </button>
254
- ))
255
- )}
256
- </div>
257
- </div>
258
- )}
259
-
260
- {editor && userVersion && (
261
- <TetronsToolbar editor={editor} version={userVersion} />
262
- )}
263
-
264
- <div
265
- ref={wrapperRef}
266
- className="editor-content-wrapper"
267
- onClick={handleEditorClick}
268
- >
269
- {editor ? (
270
- <>
271
- <TiptapEditorContent editor={editor} />
272
- <TableContextMenu editor={editor} />
273
- </>
274
- ) : (
275
- <div className="editor-loading">Loading editor...</div>
276
- )}
277
- </div>
278
- </div>
279
- );
280
- }
@@ -1,77 +0,0 @@
1
- import React, { useRef, useEffect } from "react";
2
- import { NodeViewWrapper, NodeViewRendererProps } from "@tiptap/react";
3
-
4
- interface ResizableImageProps extends NodeViewRendererProps {
5
- updateAttributes: (attrs: {
6
- width?: number | null;
7
- height?: number | null;
8
- }) => void;
9
- selected?: boolean;
10
- }
11
-
12
- const ResizableImageComponent: React.FC<ResizableImageProps> = ({
13
- node,
14
- updateAttributes,
15
- selected,
16
- }) => {
17
- const { src, alt, title, width, height } = node.attrs as {
18
- src: string;
19
- alt?: string;
20
- title?: string;
21
- width?: number | null;
22
- height?: number | null;
23
- };
24
- const wrapperRef = useRef<HTMLDivElement>(null);
25
- const imgRef = useRef<HTMLImageElement>(null);
26
-
27
- useEffect(() => {
28
- const img = imgRef.current;
29
- if (!img) return;
30
-
31
- const observer = new ResizeObserver(() => {
32
- const w = Math.round(img.offsetWidth);
33
- const h = Math.round(img.offsetHeight);
34
- updateAttributes({ width: w, height: h });
35
- });
36
-
37
- observer.observe(img);
38
- return () => observer.disconnect();
39
- }, [updateAttributes]);
40
-
41
- return (
42
- <NodeViewWrapper
43
- ref={wrapperRef}
44
- contentEditable={false}
45
- className={`resizable-image-wrapper ${
46
- selected ? "ProseMirror-selectednode" : ""
47
- }`}
48
- style={{
49
- resize: "both",
50
- overflow: "auto",
51
- border: "1px solid #ccc",
52
- padding: 2,
53
- display: "inline-block",
54
- maxWidth: "100%",
55
- }}
56
- >
57
- {/* eslint-disable-next-line @next/next/no-img-element */}
58
- <img
59
- ref={imgRef}
60
- src={src}
61
- alt={alt ?? ""}
62
- title={title ?? ""}
63
- loading="lazy"
64
- style={{
65
- width: width ? `${width}px` : "auto",
66
- height: height ? `${height}px` : "auto",
67
- display: "block",
68
- userSelect: "none",
69
- pointerEvents: "auto",
70
- }}
71
- draggable={false}
72
- />
73
- </NodeViewWrapper>
74
- );
75
- };
76
-
77
- export default ResizableImageComponent;