@cyberyozh/text-editor 0.1.6 → 0.1.8

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 (37) hide show
  1. package/dist/editor.css +46 -20
  2. package/dist/editor.css.map +1 -1
  3. package/dist/editor.js +483 -456
  4. package/dist/editor.js.map +1 -1
  5. package/dist/editor.mjs +481 -454
  6. package/dist/editor.mjs.map +1 -1
  7. package/dist/render.js +68 -29
  8. package/dist/render.js.map +1 -1
  9. package/dist/render.mjs +68 -29
  10. package/dist/render.mjs.map +1 -1
  11. package/dist/styles/index.css +46 -20
  12. package/package.json +5 -3
  13. package/src/editor/commands/items.ts +1 -1
  14. package/src/editor/components/common-modal/common-modal.css +2 -1
  15. package/src/editor/context/dialog-command.tsx +0 -2
  16. package/src/editor/controls/bubble-menu-content.tsx +6 -2
  17. package/src/editor/controls/commands-list.tsx +7 -0
  18. package/src/editor/dialogs/add-video-dialog/add-video-dialog.tsx +2 -5
  19. package/src/editor/dialogs/image-dialog/image-dialog.tsx +0 -1
  20. package/src/editor/editor.tsx +68 -44
  21. package/src/editor/handlers/dialog-handlers.ts +74 -62
  22. package/src/editor/handlers/index.ts +1 -2
  23. package/src/extensions/views/components/index.ts +1 -0
  24. package/src/extensions/views/components/table.tsx +48 -0
  25. package/src/render/static-renderer-config.tsx +16 -2
  26. package/src/styles/editor/base.css +7 -5
  27. package/src/styles/editor/states.css +4 -3
  28. package/src/styles/editor/typography.css +10 -2
  29. package/src/styles/editor/variables.css +1 -0
  30. package/src/styles/extensions/callout.css +5 -2
  31. package/src/styles/extensions/code-block.css +2 -1
  32. package/src/styles/extensions/commands-list.css +5 -1
  33. package/src/styles/extensions/custom-quote.css +5 -0
  34. package/src/styles/extensions/details.css +4 -4
  35. package/src/styles/extensions/myth-reality.css +3 -1
  36. package/src/styles/extensions/prompt.css +4 -1
  37. package/src/editor/handlers/create-image-upload-handler.ts +0 -24
@@ -1,28 +1,37 @@
1
1
  import type { Editor } from "@tiptap/react";
2
+ import { TextSelection } from "@tiptap/pm/state";
2
3
 
3
4
  import type { FigureAttrs, TiptapImageAttrs } from "../../types";
4
5
 
5
- export const createHandleImageAdd = (editor: Editor | null, closeDialogCommand: () => void) => {
6
+ export const createHandleImageAdd = (
7
+ editor: Editor | null,
8
+ closeDialogCommand: () => void,
9
+ savedPos?: number
10
+ ) => {
6
11
  return (attrs: TiptapImageAttrs) => {
7
12
  if (!editor) return;
8
13
 
9
14
  const { state } = editor;
10
- const { $from } = state.selection;
11
- const text = $from.parent.textContent;
15
+ const insertPos = savedPos ?? state.selection.from;
16
+
17
+ editor
18
+ .chain()
19
+ .focus()
20
+ .command(({ tr, state }) => {
21
+ const $pos = state.doc.resolve(insertPos);
22
+ const imageNode = state.schema.nodes.image.create({ ...attrs });
23
+
24
+ if ($pos.parent.type.name === "paragraph" && $pos.parent.textContent === "") {
25
+ tr.replaceWith($pos.before(), $pos.after(), imageNode);
26
+ tr.setSelection(TextSelection.near(tr.doc.resolve($pos.before())));
27
+ } else {
28
+ tr.insert(insertPos, imageNode);
29
+ tr.setSelection(TextSelection.near(tr.doc.resolve(insertPos)));
30
+ }
31
+ return true;
32
+ })
33
+ .run();
12
34
 
13
- if (text.lastIndexOf("/") !== -1) {
14
- editor
15
- .chain()
16
- .focus()
17
- .command(({ tr }) => {
18
- const node = editor.schema.nodes.image.create({ ...attrs });
19
- tr.replaceWith($from.before(), $from.after(), node);
20
- return true;
21
- })
22
- .run();
23
- } else {
24
- editor?.chain().focus().setFigure(attrs).run();
25
- }
26
35
  closeDialogCommand();
27
36
  };
28
37
  };
@@ -33,6 +42,7 @@ export const createHandleImageEdit = (
33
42
  pos?: number
34
43
  ) => {
35
44
  return (attrs: Partial<FigureAttrs>) => {
45
+ console.log("👀 ~ createHandleImageEdit ~ attrs:", attrs);
36
46
  if (!editor || pos === undefined) return;
37
47
 
38
48
  editor.chain().focus().setNodeSelection(pos).updateFigure(attrs).run();
@@ -40,30 +50,44 @@ export const createHandleImageEdit = (
40
50
  };
41
51
  };
42
52
 
43
- export const createHandleVideoAdd = (editor: Editor | null, closeDialogCommand: () => void) => {
53
+ export const createHandleVideoAdd = (
54
+ editor: Editor | null,
55
+ closeDialogCommand: () => void,
56
+ savedPos?: number
57
+ ) => {
44
58
  return (url: string) => {
45
59
  if (!editor) return;
46
60
 
47
61
  const { state } = editor;
48
- const { $from } = state.selection;
62
+ const insertPos = savedPos ?? state.selection.from;
49
63
 
50
64
  editor
51
65
  .chain()
52
66
  .focus()
53
- .command(({ tr }) => {
54
- tr.replaceWith(
55
- $from.before(),
56
- $from.after(),
57
- editor.schema.nodes.video.create({ src: url })
58
- );
67
+ .command(({ tr, state }) => {
68
+ const $pos = state.doc.resolve(insertPos);
69
+ const videoNode = state.schema.nodes.video.create({ src: url });
70
+
71
+ if ($pos.parent.type.name === "paragraph" && $pos.parent.textContent === "") {
72
+ tr.replaceWith($pos.before(), $pos.after(), videoNode);
73
+ tr.setSelection(TextSelection.near(tr.doc.resolve($pos.before())));
74
+ } else {
75
+ tr.insert(insertPos, videoNode);
76
+ tr.setSelection(TextSelection.near(tr.doc.resolve(insertPos)));
77
+ }
59
78
  return true;
60
79
  })
61
80
  .run();
81
+
62
82
  closeDialogCommand();
63
83
  };
64
84
  };
65
85
 
66
- export const createHandleLinkAdd = (editor: Editor | null, closeDialogCommand: () => void) => {
86
+ export const createHandleLinkAdd = (
87
+ editor: Editor | null,
88
+ closeDialogCommand: () => void,
89
+ savedPos?: number
90
+ ) => {
67
91
  return (title: string, url: string) => {
68
92
  if (!editor) return;
69
93
 
@@ -81,32 +105,30 @@ export const createHandleLinkAdd = (editor: Editor | null, closeDialogCommand: (
81
105
  })
82
106
  .run();
83
107
  } else {
108
+ const { state } = editor;
109
+ const insertPos = savedPos ?? state.selection.from;
110
+
84
111
  editor
85
112
  .chain()
86
113
  .focus()
87
- .command(({ tr, state }) => {
88
- const { $from } = state.selection;
89
- const text = $from.parent.textContent;
90
- const nodeStart = $from.start();
91
- const beforeCursor = text.substring(0, $from.pos - nodeStart);
92
- const slashIndex = beforeCursor.lastIndexOf("/");
93
-
94
- if (slashIndex !== -1) {
95
- tr.delete(nodeStart + slashIndex, $from.pos);
96
- }
97
-
114
+ .command(({ tr }) => {
115
+ const linkMark = editor.schema.marks.link.create({ href: url });
116
+ const textNode = editor.schema.text(title, [linkMark]);
117
+ tr.insert(insertPos, textNode);
118
+ tr.setSelection(TextSelection.create(tr.doc, insertPos, insertPos + title.length));
98
119
  return true;
99
120
  })
100
- .extendMarkRange("link")
101
- .setLink({ href: url })
102
- .insertContent(title)
103
121
  .run();
104
122
  }
105
123
  closeDialogCommand();
106
124
  };
107
125
  };
108
126
 
109
- export const createHandleKatexAdd = (editor: Editor | null, closeDialogCommand: () => void) => {
127
+ export const createHandleKatexAdd = (
128
+ editor: Editor | null,
129
+ closeDialogCommand: () => void,
130
+ savedPos?: number
131
+ ) => {
110
132
  return (latex: string, pos?: number) => {
111
133
  if (!editor) return;
112
134
 
@@ -114,28 +136,18 @@ export const createHandleKatexAdd = (editor: Editor | null, closeDialogCommand:
114
136
  editor.chain().setNodeSelection(pos).updateInlineMath({ latex }).focus().run();
115
137
  } else {
116
138
  const { state } = editor;
117
- const { $from } = state.selection;
118
- const text = $from.parent.textContent;
119
- const nodeStart = $from.start();
120
- const beforeCursor = text.substring(0, $from.pos - nodeStart);
121
- const slashIndex = beforeCursor.lastIndexOf("/");
122
-
123
- if (slashIndex !== -1) {
124
- const deleteFrom = nodeStart + slashIndex;
125
- const deleteTo = $from.pos;
126
-
127
- editor
128
- .chain()
129
- .focus()
130
- .command(({ tr, state }) => {
131
- const inlineMathNode = state.schema.nodes.inlineMath.create({ latex });
132
- tr.replaceRangeWith(deleteFrom, deleteTo, inlineMathNode);
133
- return true;
134
- })
135
- .run();
136
- } else {
137
- editor.chain().insertInlineMath({ latex }).focus().run();
138
- }
139
+ const insertPos = savedPos ?? state.selection.from;
140
+
141
+ editor
142
+ .chain()
143
+ .focus()
144
+ .command(({ tr, state }) => {
145
+ const inlineMathNode = state.schema.nodes.inlineMath.create({ latex });
146
+ tr.insert(insertPos, inlineMathNode);
147
+ tr.setSelection(TextSelection.near(tr.doc.resolve(insertPos)));
148
+ return true;
149
+ })
150
+ .run();
139
151
  }
140
152
 
141
153
  closeDialogCommand();
@@ -1,8 +1,7 @@
1
- export { createImageUploadHandler } from './create-image-upload-handler';
2
1
  export {
3
2
  createHandleImageAdd,
4
3
  createHandleKatexAdd,
5
4
  createHandleLinkAdd,
6
5
  createHandleVideoAdd,
7
6
  createHandleImageEdit,
8
- } from './dialog-handlers';
7
+ } from "./dialog-handlers";
@@ -4,4 +4,5 @@ export { CopyableText } from "./copyable-text";
4
4
  export { CustomQuote, CustomQuoteText, CustomQuoteAuthor, CustomQuoteSign } from "./custom-quote";
5
5
  export { Figure } from "./figure";
6
6
  export { PromptHeader, PromptContent } from "./prompt";
7
+ export { Table } from "./table";
7
8
  export { Video } from "./video";
@@ -0,0 +1,48 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ interface TableProps {
4
+ children: ReactNode;
5
+ }
6
+
7
+ /**
8
+ * Table component for static rendering
9
+ * Applies column widths from Tiptap table cells
10
+ */
11
+ export function Table({ children }: TableProps) {
12
+ let colWidths: number[] = [];
13
+
14
+ if (Array.isArray(children) && children.length > 0) {
15
+ const firstRow = children[0];
16
+
17
+ if (firstRow?.props?.children) {
18
+ const cells = Array.isArray(firstRow.props.children)
19
+ ? firstRow.props.children
20
+ : [firstRow.props.children];
21
+
22
+ colWidths = cells
23
+ .map((cell: any) => cell?.props?.node?.attrs?.colwidth?.[0])
24
+ .filter((width: number | undefined) => width !== undefined);
25
+ }
26
+ }
27
+
28
+ const tableWidth =
29
+ colWidths.length > 0 ? colWidths.reduce((total, width) => total + width, 0) : null;
30
+
31
+ return (
32
+ <div className="tableWrapper">
33
+ <table style={tableWidth ? { width: `${tableWidth}px` } : undefined}>
34
+ {colWidths.length > 0 && (
35
+ <colgroup>
36
+ {colWidths.map((width, index) => (
37
+ <col
38
+ key={index}
39
+ style={{ width: `${width}px` }}
40
+ />
41
+ ))}
42
+ </colgroup>
43
+ )}
44
+ <tbody>{children}</tbody>
45
+ </table>
46
+ </div>
47
+ );
48
+ }
@@ -24,6 +24,7 @@ import {
24
24
  CustomQuoteSign,
25
25
  PromptHeader,
26
26
  PromptContent,
27
+ Table,
27
28
  Video,
28
29
  Figure,
29
30
  } from "@/extensions/views/components";
@@ -94,11 +95,24 @@ export const getNodeMapping = (getImageById?: (fileId: string) => string | null)
94
95
  video({ node }: any) {
95
96
  return <Video src={node.attrs.src} />;
96
97
  },
98
+ table({ children }: any) {
99
+ return <Table>{children}</Table>;
100
+ },
97
101
  figure({ node }: any) {
98
- return <Figure {...node.attrs} getImageById={getImageById} />;
102
+ return (
103
+ <Figure
104
+ {...node.attrs}
105
+ getImageById={getImageById}
106
+ />
107
+ );
99
108
  },
100
109
  image({ node }: any) {
101
- return <Figure {...node.attrs} getImageById={getImageById} />;
110
+ return (
111
+ <Figure
112
+ {...node.attrs}
113
+ getImageById={getImageById}
114
+ />
115
+ );
102
116
  },
103
117
  inlineMath({ node }: any) {
104
118
  const latex = node.attrs.latex || "";
@@ -1,9 +1,6 @@
1
1
  .tiptap-content-wrapper {
2
2
  position: relative;
3
3
  margin: 0 !important;
4
- }
5
-
6
- .tiptap {
7
4
  padding: var(--tiptap-spacing);
8
5
  border: 1px solid var(--tiptap-color-border);
9
6
  border-radius: var(--tiptap-radius);
@@ -11,12 +8,17 @@
11
8
  font-family: var(--tiptap-font-family);
12
9
  }
13
10
 
14
- .tiptap:focus {
11
+ .tiptap-content-wrapper:focus {
15
12
  border-color: var(--tiptap-color-primary-light);
16
13
  outline: none;
17
14
  }
18
15
 
19
- .tiptap-content-wrapper.has-error .tiptap {
16
+ .tiptap-content-wrapper .ProseMirror {
17
+ border: none;
18
+ outline: none;
19
+ }
20
+
21
+ .tiptap-content-wrapper.has-error {
20
22
  border-color: var(--tiptap-color-primary-error);
21
23
  }
22
24
 
@@ -44,11 +44,12 @@
44
44
 
45
45
  /* Disabled state styles */
46
46
  .tiptap-content-wrapper.is-disabled {
47
- opacity: 0.6;
47
+ padding: var(--tiptap-spacing);
48
48
  cursor: not-allowed;
49
+ opacity: 0.6;
49
50
  pointer-events: none;
50
51
  }
51
52
 
52
- .tiptap-content-wrapper.is-disabled .ProseMirror {
53
- background-color: var(--tiptap-color-grey-1, #f5f5f5);
53
+ .tiptap-content-wrapper.is-disabled {
54
+ background-color: var(--tiptap-color-bg-disabled);
54
55
  }
@@ -3,11 +3,11 @@
3
3
  font-weight: 400;
4
4
  font-size: var(--tiptap-font-size);
5
5
  line-height: 140%;
6
- letter-spacing: 0;
6
+ letter-spacing: normal;
7
7
  }
8
8
 
9
9
  .tiptap > * {
10
- letter-spacing: 0;
10
+ letter-spacing: normal;
11
11
  }
12
12
 
13
13
  .tiptap p:not(:last-child),
@@ -17,6 +17,13 @@
17
17
  margin-bottom: var(--tiptap-spacing);
18
18
  }
19
19
 
20
+ .tiptap h1 {
21
+ font-weight: 600;
22
+ font-size: 40px;
23
+ line-height: 110%;
24
+ letter-spacing: 0;
25
+ }
26
+
20
27
  .tiptap h2 {
21
28
  font-weight: 600;
22
29
  font-size: 36px;
@@ -76,6 +83,7 @@
76
83
  .tiptap ul,
77
84
  .tiptap ol {
78
85
  padding-left: 32px;
86
+ font-size: var(--tiptap-font-size);
79
87
  }
80
88
 
81
89
  .tiptap ul:not(:last-child),
@@ -8,6 +8,7 @@
8
8
  --tiptap-color-link: #0059df;
9
9
  --tiptap-color-primary-error: #f04438;
10
10
  --tiptap-color-border: #f2f4f7;
11
+ --tiptap-color-bg-disabled: #f7f7f8;
11
12
  --tiptap-color-divider: #aaabb7;
12
13
  --tiptap-color-text-disabled: #aaabb7;
13
14
  --tiptap-color-text-placeholder: #aaabb7;
@@ -22,19 +22,20 @@
22
22
 
23
23
  .callout-node {
24
24
  display: flex;
25
+ align-items: baseline;
25
26
  margin: 32px 0;
26
27
  padding: 16px;
27
28
  gap: 10px;
28
29
  border-radius: 8px;
29
30
  }
30
-
31
+ /*
31
32
  .callout-node:has(.callout-content h2),
32
33
  .callout-node:has(.callout-content h3),
33
34
  .callout-node:has(.callout-content h4),
34
35
  .callout-node:has(.callout-content h5),
35
36
  .callout-node:has(.callout-content h6) {
36
37
  align-items: baseline;
37
- }
38
+ } */
38
39
 
39
40
  .callout-node p {
40
41
  font-size: 16px;
@@ -103,6 +104,8 @@
103
104
  .callout-content-wrapper {
104
105
  flex: 1;
105
106
  min-width: 0;
107
+ font-size: var(--tiptap-font-size);
108
+ line-height: 1;
106
109
  }
107
110
 
108
111
  .callout-content {
@@ -4,7 +4,7 @@
4
4
  --tiptap-code-block-text: #e0e0e0;
5
5
  --tiptap-code-block-copy-button: #eaecef;
6
6
  --tiptap-code-block-copy-button-hover: #cdc8c8;
7
- --tiptap-code-block-font-family: 'Fira Code', monospace;
7
+ --tiptap-code-block-font-family: "Fira Code", monospace;
8
8
  }
9
9
 
10
10
  .tiptap-code-block-wrapper {
@@ -55,6 +55,7 @@
55
55
  flex: 1;
56
56
  max-width: 50%;
57
57
  color: var(--tiptap-code-block-text);
58
+ font-size: 14px;
58
59
  font-family: var(--tiptap-code-block-font-family);
59
60
  }
60
61
 
@@ -1,6 +1,10 @@
1
+ .react-renderer:has(.commands-list-container) {
2
+ z-index: 10000;
3
+ }
4
+
1
5
  .commands-list-container {
2
6
  display: flex;
3
- z-index: 50;
7
+ position: relative;
4
8
  flex-direction: column;
5
9
  width: 280px;
6
10
  max-height: 300px;
@@ -30,6 +30,11 @@
30
30
  flex-direction: column;
31
31
  max-width: 700px;
32
32
  margin: 0 auto 16px;
33
+ font-style: normal;
34
+ font-weight: 500;
35
+ font-size: 18px;
36
+ line-height: 140%;
37
+ letter-spacing: -0.36px;
33
38
  }
34
39
 
35
40
  [data-custom-quote-text] .is-empty::before {
@@ -1,4 +1,3 @@
1
- /* Editor view with custom button */
2
1
  [data-type="details"] {
3
2
  display: flex;
4
3
  align-items: baseline;
@@ -35,7 +34,6 @@
35
34
  transform: rotate(90deg);
36
35
  }
37
36
 
38
- /* Native HTML details element (server-rendered) */
39
37
  .tiptap details.details {
40
38
  align-items: baseline;
41
39
  margin: 24px 0;
@@ -53,12 +51,15 @@
53
51
  border: none;
54
52
  outline: none;
55
53
  background: transparent;
54
+ font-weight: 500;
55
+ font-size: 18px;
56
+ line-height: 140%;
57
+ letter-spacing: -0.36px;
56
58
  list-style: none;
57
59
  cursor: pointer;
58
60
  transition: all 150ms ease-in-out;
59
61
  }
60
62
 
61
- /* Custom marker styling using ::before since ::marker doesn't support transform */
62
63
  .tiptap details.details summary::before {
63
64
  display: inline-block;
64
65
  flex-shrink: 0;
@@ -72,7 +73,6 @@
72
73
  transform: rotate(90deg);
73
74
  }
74
75
 
75
- /* Hide the default marker */
76
76
  .tiptap details.details summary::-webkit-details-marker {
77
77
  display: none;
78
78
  }
@@ -49,11 +49,13 @@
49
49
 
50
50
  .myth-reality-block-content p:first-child {
51
51
  margin-bottom: 8px;
52
- font-weight: 600;
52
+ font-weight: 600 !important;
53
53
  }
54
54
 
55
+ .myth-reality-block-content p,
55
56
  .myth-reality-block-content ul p,
56
57
  .myth-reality-block-content li p {
57
58
  margin-bottom: 0;
59
+ font-weight: 400;
58
60
  font-size: var(--tiptap-font-size);
59
61
  }
@@ -37,8 +37,11 @@
37
37
  justify-content: space-between;
38
38
  margin-bottom: 16px;
39
39
  gap: 8px;
40
+ font-style: normal;
40
41
  font-weight: 600;
41
- font-size: var(--tiptap-prompt-header-font-size);
42
+ font-size: var(--tiptap-font-size);
43
+ line-height: 140%;
44
+ letter-spacing: -0.36px;
42
45
  }
43
46
 
44
47
  .prompt-content-wrapper {
@@ -1,24 +0,0 @@
1
- import { ImageUploadHandler } from "../../types";
2
-
3
- /**
4
- * Creates an image upload handler that wraps a custom upload action
5
- * Supports two patterns:
6
- * 1. Backend with fileId: { fileId: "file-123" }
7
- * 2. Direct CDN: { fileUrl: "https://cdn.com/image.jpg" }
8
- */
9
- export function createImageUploadHandler(
10
- uploadAction: (file: File) => Promise<{ data?: { fileId?: string; fileUrl?: string } }>
11
- ): ImageUploadHandler {
12
- return async (file: File) => {
13
- const { data } = await uploadAction(file);
14
-
15
- if (!data?.fileId && !data?.fileUrl) {
16
- throw new Error("Failed to upload file: must return fileId or fileUrl");
17
- }
18
-
19
- return {
20
- fileId: data.fileId,
21
- fileUrl: data.fileUrl,
22
- };
23
- };
24
- }