doclific 0.1.0

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 (231) hide show
  1. package/.gitattributes +2 -0
  2. package/.prettierignore +5 -0
  3. package/.prettierrc +9 -0
  4. package/.vscode/settings.json +13 -0
  5. package/dist/bin/doclific.d.ts +3 -0
  6. package/dist/bin/doclific.d.ts.map +1 -0
  7. package/dist/bin/doclific.js +11 -0
  8. package/dist/core/codebase.js +31 -0
  9. package/dist/core/docs.js +75 -0
  10. package/dist/core/git.js +47 -0
  11. package/dist/server/index.d.ts +2 -0
  12. package/dist/server/index.d.ts.map +1 -0
  13. package/dist/server/index.js +46 -0
  14. package/dist/server/router.d.ts +9 -0
  15. package/dist/server/router.d.ts.map +1 -0
  16. package/dist/server/router.js +55 -0
  17. package/frontend/README.md +73 -0
  18. package/frontend/components.json +24 -0
  19. package/frontend/eslint.config.js +23 -0
  20. package/frontend/index.html +25 -0
  21. package/frontend/package-lock.json +15754 -0
  22. package/frontend/package.json +122 -0
  23. package/frontend/public/logo.svg +1 -0
  24. package/frontend/src/App.tsx +21 -0
  25. package/frontend/src/components/app-sidebar.tsx +393 -0
  26. package/frontend/src/components/editor/editor-base-kit.tsx +43 -0
  27. package/frontend/src/components/editor/editor-kit.tsx +93 -0
  28. package/frontend/src/components/editor/plugins/align-base-kit.tsx +16 -0
  29. package/frontend/src/components/editor/plugins/align-kit.tsx +18 -0
  30. package/frontend/src/components/editor/plugins/autoformat-kit.tsx +236 -0
  31. package/frontend/src/components/editor/plugins/basic-blocks-base-kit.tsx +35 -0
  32. package/frontend/src/components/editor/plugins/basic-blocks-kit.tsx +88 -0
  33. package/frontend/src/components/editor/plugins/basic-marks-base-kit.tsx +27 -0
  34. package/frontend/src/components/editor/plugins/basic-marks-kit.tsx +41 -0
  35. package/frontend/src/components/editor/plugins/basic-nodes-kit.tsx +6 -0
  36. package/frontend/src/components/editor/plugins/block-menu-kit.tsx +14 -0
  37. package/frontend/src/components/editor/plugins/block-placeholder-kit.tsx +17 -0
  38. package/frontend/src/components/editor/plugins/block-selection-kit.tsx +32 -0
  39. package/frontend/src/components/editor/plugins/callout-base-kit.tsx +7 -0
  40. package/frontend/src/components/editor/plugins/callout-kit.tsx +7 -0
  41. package/frontend/src/components/editor/plugins/code-block-base-kit.tsx +23 -0
  42. package/frontend/src/components/editor/plugins/code-block-kit.tsx +26 -0
  43. package/frontend/src/components/editor/plugins/codebase-kit.tsx +23 -0
  44. package/frontend/src/components/editor/plugins/column-base-kit.tsx +11 -0
  45. package/frontend/src/components/editor/plugins/column-kit.tsx +10 -0
  46. package/frontend/src/components/editor/plugins/comment-base-kit.tsx +7 -0
  47. package/frontend/src/components/editor/plugins/comment-kit.tsx +97 -0
  48. package/frontend/src/components/editor/plugins/cursor-overlay-kit.tsx +13 -0
  49. package/frontend/src/components/editor/plugins/date-base-kit.tsx +5 -0
  50. package/frontend/src/components/editor/plugins/date-kit.tsx +7 -0
  51. package/frontend/src/components/editor/plugins/discussion-kit.tsx +148 -0
  52. package/frontend/src/components/editor/plugins/dnd-kit.tsx +28 -0
  53. package/frontend/src/components/editor/plugins/docx-kit.tsx +6 -0
  54. package/frontend/src/components/editor/plugins/emoji-kit.tsx +13 -0
  55. package/frontend/src/components/editor/plugins/excalidraw-kit.tsx +9 -0
  56. package/frontend/src/components/editor/plugins/exit-break-kit.tsx +12 -0
  57. package/frontend/src/components/editor/plugins/floating-toolbar-kit.tsx +19 -0
  58. package/frontend/src/components/editor/plugins/font-base-kit.tsx +20 -0
  59. package/frontend/src/components/editor/plugins/font-kit.tsx +29 -0
  60. package/frontend/src/components/editor/plugins/indent-base-kit.tsx +19 -0
  61. package/frontend/src/components/editor/plugins/indent-kit.tsx +22 -0
  62. package/frontend/src/components/editor/plugins/line-height-base-kit.tsx +14 -0
  63. package/frontend/src/components/editor/plugins/line-height-kit.tsx +16 -0
  64. package/frontend/src/components/editor/plugins/link-base-kit.tsx +5 -0
  65. package/frontend/src/components/editor/plugins/link-kit.tsx +15 -0
  66. package/frontend/src/components/editor/plugins/list-base-kit.tsx +23 -0
  67. package/frontend/src/components/editor/plugins/list-kit.tsx +26 -0
  68. package/frontend/src/components/editor/plugins/markdown-kit.tsx +46 -0
  69. package/frontend/src/components/editor/plugins/math-base-kit.tsx +11 -0
  70. package/frontend/src/components/editor/plugins/math-kit.tsx +13 -0
  71. package/frontend/src/components/editor/plugins/media-base-kit.tsx +31 -0
  72. package/frontend/src/components/editor/plugins/media-kit.tsx +43 -0
  73. package/frontend/src/components/editor/plugins/mention-base-kit.tsx +7 -0
  74. package/frontend/src/components/editor/plugins/mention-kit.tsx +15 -0
  75. package/frontend/src/components/editor/plugins/slash-kit.tsx +18 -0
  76. package/frontend/src/components/editor/plugins/suggestion-base-kit.tsx +7 -0
  77. package/frontend/src/components/editor/plugins/suggestion-kit.tsx +90 -0
  78. package/frontend/src/components/editor/plugins/table-base-kit.tsx +20 -0
  79. package/frontend/src/components/editor/plugins/table-kit.tsx +22 -0
  80. package/frontend/src/components/editor/plugins/toc-base-kit.tsx +5 -0
  81. package/frontend/src/components/editor/plugins/toc-kit.tsx +14 -0
  82. package/frontend/src/components/editor/plugins/toggle-base-kit.tsx +7 -0
  83. package/frontend/src/components/editor/plugins/toggle-kit.tsx +11 -0
  84. package/frontend/src/components/editor/transforms.ts +194 -0
  85. package/frontend/src/components/markdown-to-slate-demo.tsx +50 -0
  86. package/frontend/src/components/mode-toggle.tsx +15 -0
  87. package/frontend/src/components/theme-provider.tsx +73 -0
  88. package/frontend/src/components/ui/alert-dialog.tsx +155 -0
  89. package/frontend/src/components/ui/align-toolbar-button.tsx +84 -0
  90. package/frontend/src/components/ui/avatar.tsx +51 -0
  91. package/frontend/src/components/ui/block-context-menu.tsx +199 -0
  92. package/frontend/src/components/ui/block-discussion.tsx +365 -0
  93. package/frontend/src/components/ui/block-draggable.tsx +512 -0
  94. package/frontend/src/components/ui/block-list-static.tsx +80 -0
  95. package/frontend/src/components/ui/block-list.tsx +87 -0
  96. package/frontend/src/components/ui/block-selection.tsx +42 -0
  97. package/frontend/src/components/ui/block-suggestion.tsx +473 -0
  98. package/frontend/src/components/ui/blockquote-node-static.tsx +11 -0
  99. package/frontend/src/components/ui/blockquote-node.tsx +13 -0
  100. package/frontend/src/components/ui/button.tsx +62 -0
  101. package/frontend/src/components/ui/calendar.tsx +218 -0
  102. package/frontend/src/components/ui/callout-node-static.tsx +36 -0
  103. package/frontend/src/components/ui/callout-node.tsx +63 -0
  104. package/frontend/src/components/ui/caption.tsx +63 -0
  105. package/frontend/src/components/ui/checkbox.tsx +30 -0
  106. package/frontend/src/components/ui/code-block-node-static.tsx +35 -0
  107. package/frontend/src/components/ui/code-block-node.tsx +287 -0
  108. package/frontend/src/components/ui/code-node-static.tsx +15 -0
  109. package/frontend/src/components/ui/code-node.tsx +17 -0
  110. package/frontend/src/components/ui/codebase-snippet-node.tsx +237 -0
  111. package/frontend/src/components/ui/column-node-static.tsx +29 -0
  112. package/frontend/src/components/ui/column-node.tsx +317 -0
  113. package/frontend/src/components/ui/command.tsx +182 -0
  114. package/frontend/src/components/ui/comment-node-static.tsx +15 -0
  115. package/frontend/src/components/ui/comment-node.tsx +45 -0
  116. package/frontend/src/components/ui/comment-toolbar-button.tsx +24 -0
  117. package/frontend/src/components/ui/comment.tsx +618 -0
  118. package/frontend/src/components/ui/context-menu.tsx +250 -0
  119. package/frontend/src/components/ui/cursor-overlay.tsx +66 -0
  120. package/frontend/src/components/ui/date-node-static.tsx +45 -0
  121. package/frontend/src/components/ui/date-node.tsx +93 -0
  122. package/frontend/src/components/ui/dialog.tsx +143 -0
  123. package/frontend/src/components/ui/dropdown-menu.tsx +255 -0
  124. package/frontend/src/components/ui/dynamic-icon.tsx +12 -0
  125. package/frontend/src/components/ui/editor-static.tsx +53 -0
  126. package/frontend/src/components/ui/editor.tsx +130 -0
  127. package/frontend/src/components/ui/emoji-node.tsx +69 -0
  128. package/frontend/src/components/ui/emoji-toolbar-button.tsx +628 -0
  129. package/frontend/src/components/ui/equation-node-static.tsx +98 -0
  130. package/frontend/src/components/ui/equation-node.tsx +235 -0
  131. package/frontend/src/components/ui/equation-toolbar-button.tsx +25 -0
  132. package/frontend/src/components/ui/excalidraw-node.tsx +36 -0
  133. package/frontend/src/components/ui/export-toolbar-button.tsx +174 -0
  134. package/frontend/src/components/ui/file-selector.tsx +339 -0
  135. package/frontend/src/components/ui/floating-toolbar-buttons.tsx +73 -0
  136. package/frontend/src/components/ui/floating-toolbar.tsx +85 -0
  137. package/frontend/src/components/ui/font-color-toolbar-button.tsx +831 -0
  138. package/frontend/src/components/ui/font-size-toolbar-button.tsx +152 -0
  139. package/frontend/src/components/ui/heading-node-static.tsx +68 -0
  140. package/frontend/src/components/ui/heading-node.tsx +58 -0
  141. package/frontend/src/components/ui/highlight-node-static.tsx +11 -0
  142. package/frontend/src/components/ui/highlight-node.tsx +13 -0
  143. package/frontend/src/components/ui/history-toolbar-button.tsx +50 -0
  144. package/frontend/src/components/ui/hr-node-static.tsx +20 -0
  145. package/frontend/src/components/ui/hr-node.tsx +33 -0
  146. package/frontend/src/components/ui/import-toolbar-button.tsx +97 -0
  147. package/frontend/src/components/ui/indent-toolbar-button.tsx +30 -0
  148. package/frontend/src/components/ui/inline-combobox.tsx +414 -0
  149. package/frontend/src/components/ui/input.tsx +21 -0
  150. package/frontend/src/components/ui/insert-toolbar-button.tsx +254 -0
  151. package/frontend/src/components/ui/kbd-node-static.tsx +15 -0
  152. package/frontend/src/components/ui/kbd-node.tsx +17 -0
  153. package/frontend/src/components/ui/layout-header.tsx +35 -0
  154. package/frontend/src/components/ui/line-height-toolbar-button.tsx +68 -0
  155. package/frontend/src/components/ui/link-node-static.tsx +21 -0
  156. package/frontend/src/components/ui/link-node.tsx +39 -0
  157. package/frontend/src/components/ui/link-toolbar-button.tsx +22 -0
  158. package/frontend/src/components/ui/link-toolbar.tsx +206 -0
  159. package/frontend/src/components/ui/list-toolbar-button.tsx +204 -0
  160. package/frontend/src/components/ui/mark-toolbar-button.tsx +19 -0
  161. package/frontend/src/components/ui/media-audio-node-static.tsx +17 -0
  162. package/frontend/src/components/ui/media-audio-node.tsx +39 -0
  163. package/frontend/src/components/ui/media-embed-node.tsx +136 -0
  164. package/frontend/src/components/ui/media-file-node-static.tsx +29 -0
  165. package/frontend/src/components/ui/media-file-node.tsx +47 -0
  166. package/frontend/src/components/ui/media-image-node-static.tsx +39 -0
  167. package/frontend/src/components/ui/media-image-node.tsx +80 -0
  168. package/frontend/src/components/ui/media-placeholder-node.tsx +249 -0
  169. package/frontend/src/components/ui/media-preview-dialog.tsx +152 -0
  170. package/frontend/src/components/ui/media-toolbar-button.tsx +225 -0
  171. package/frontend/src/components/ui/media-toolbar.tsx +115 -0
  172. package/frontend/src/components/ui/media-upload-toast.tsx +66 -0
  173. package/frontend/src/components/ui/media-video-node-static.tsx +30 -0
  174. package/frontend/src/components/ui/media-video-node.tsx +121 -0
  175. package/frontend/src/components/ui/mention-node-static.tsx +36 -0
  176. package/frontend/src/components/ui/mention-node.tsx +194 -0
  177. package/frontend/src/components/ui/mode-toolbar-button.tsx +123 -0
  178. package/frontend/src/components/ui/more-toolbar-button.tsx +80 -0
  179. package/frontend/src/components/ui/paragraph-node-static.tsx +13 -0
  180. package/frontend/src/components/ui/paragraph-node.tsx +15 -0
  181. package/frontend/src/components/ui/popover.tsx +46 -0
  182. package/frontend/src/components/ui/resize-handle.tsx +87 -0
  183. package/frontend/src/components/ui/separator.tsx +28 -0
  184. package/frontend/src/components/ui/sheet.tsx +139 -0
  185. package/frontend/src/components/ui/sidebar.tsx +726 -0
  186. package/frontend/src/components/ui/skeleton.tsx +13 -0
  187. package/frontend/src/components/ui/slash-node.tsx +233 -0
  188. package/frontend/src/components/ui/sonner.tsx +38 -0
  189. package/frontend/src/components/ui/suggestion-node-static.tsx +35 -0
  190. package/frontend/src/components/ui/suggestion-node.tsx +162 -0
  191. package/frontend/src/components/ui/suggestion-toolbar-button.tsx +25 -0
  192. package/frontend/src/components/ui/table-icons.tsx +862 -0
  193. package/frontend/src/components/ui/table-node-static.tsx +98 -0
  194. package/frontend/src/components/ui/table-node.tsx +656 -0
  195. package/frontend/src/components/ui/table-toolbar-button.tsx +264 -0
  196. package/frontend/src/components/ui/toc-node-static.tsx +92 -0
  197. package/frontend/src/components/ui/toc-node.tsx +55 -0
  198. package/frontend/src/components/ui/toggle-node-static.tsx +18 -0
  199. package/frontend/src/components/ui/toggle-node.tsx +36 -0
  200. package/frontend/src/components/ui/toggle-toolbar-button.tsx +22 -0
  201. package/frontend/src/components/ui/toolbar.tsx +387 -0
  202. package/frontend/src/components/ui/tooltip.tsx +59 -0
  203. package/frontend/src/components/ui/turn-into-toolbar-button.tsx +188 -0
  204. package/frontend/src/hooks/use-debounce.ts +18 -0
  205. package/frontend/src/hooks/use-is-touch-device.ts +24 -0
  206. package/frontend/src/hooks/use-mobile.ts +19 -0
  207. package/frontend/src/hooks/use-mounted.ts +11 -0
  208. package/frontend/src/hooks/use-upload-file.ts +128 -0
  209. package/frontend/src/index.css +128 -0
  210. package/frontend/src/layout.tsx +42 -0
  211. package/frontend/src/lib/markdown-joiner-transform.ts +239 -0
  212. package/frontend/src/lib/orpc.ts +13 -0
  213. package/frontend/src/lib/uploadthing.ts +19 -0
  214. package/frontend/src/lib/utils.ts +6 -0
  215. package/frontend/src/main.tsx +13 -0
  216. package/frontend/src/pages/editor.tsx +44 -0
  217. package/frontend/src/types/docs.d.ts +6 -0
  218. package/frontend/src/types/global.d.ts +9 -0
  219. package/frontend/src/types/router.d.ts +4 -0
  220. package/frontend/tsconfig.app.json +33 -0
  221. package/frontend/tsconfig.json +10 -0
  222. package/frontend/tsconfig.node.json +26 -0
  223. package/frontend/vite.config.ts +14 -0
  224. package/package.json +30 -0
  225. package/src/bin/doclific.ts +17 -0
  226. package/src/core/codebase.ts +39 -0
  227. package/src/core/docs.ts +90 -0
  228. package/src/core/git.ts +48 -0
  229. package/src/server/index.ts +55 -0
  230. package/src/server/router.ts +65 -0
  231. package/tsconfig.json +15 -0
@@ -0,0 +1,42 @@
1
+
2
+
3
+ import { DndPlugin } from '@platejs/dnd';
4
+ import { useBlockSelected } from '@platejs/selection/react';
5
+ import { cva } from 'class-variance-authority';
6
+ import { type PlateElementProps, usePluginOption } from 'platejs/react';
7
+
8
+ export const blockSelectionVariants = cva(
9
+ 'pointer-events-none absolute inset-0 z-1 bg-brand/[.13] transition-opacity',
10
+ {
11
+ defaultVariants: {
12
+ active: true,
13
+ },
14
+ variants: {
15
+ active: {
16
+ false: 'opacity-0',
17
+ true: 'opacity-100',
18
+ },
19
+ },
20
+ }
21
+ );
22
+
23
+ export function BlockSelection(props: PlateElementProps) {
24
+ const isBlockSelected = useBlockSelected();
25
+ const isDragging = usePluginOption(DndPlugin, 'isDragging');
26
+
27
+ if (
28
+ !isBlockSelected ||
29
+ props.plugin.key === 'tr' ||
30
+ props.plugin.key === 'table'
31
+ )
32
+ return null;
33
+
34
+ return (
35
+ <div
36
+ className={blockSelectionVariants({
37
+ active: isBlockSelected && !isDragging,
38
+ })}
39
+ data-slot="block-selection"
40
+ />
41
+ );
42
+ }
@@ -0,0 +1,473 @@
1
+ import React from 'react';
2
+
3
+ import type { TResolvedSuggestion } from '@platejs/suggestion';
4
+
5
+ import {
6
+ acceptSuggestion,
7
+ getSuggestionKey,
8
+ keyId2SuggestionId,
9
+ rejectSuggestion,
10
+ } from '@platejs/suggestion';
11
+ import { SuggestionPlugin } from '@platejs/suggestion/react';
12
+ import { CheckIcon, XIcon } from 'lucide-react';
13
+ import {
14
+ type NodeEntry,
15
+ type Path,
16
+ type TElement,
17
+ type TSuggestionText,
18
+ ElementApi,
19
+ KEYS,
20
+ PathApi,
21
+ TextApi,
22
+ } from 'platejs';
23
+ import { useEditorPlugin, usePluginOption } from 'platejs/react';
24
+
25
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
26
+ import { Button } from '@/components/ui/button';
27
+ import {
28
+ type TDiscussion,
29
+ discussionPlugin,
30
+ } from '@/components/editor/plugins/discussion-kit';
31
+ import { suggestionPlugin } from '@/components/editor/plugins/suggestion-kit';
32
+
33
+ import {
34
+ type TComment,
35
+ Comment,
36
+ CommentCreateForm,
37
+ formatCommentDate,
38
+ } from './comment';
39
+
40
+ export interface ResolvedSuggestion extends TResolvedSuggestion {
41
+ comments: TComment[];
42
+ }
43
+
44
+ const BLOCK_SUGGESTION = '__block__';
45
+
46
+ const TYPE_TEXT_MAP: Record<string, (node?: TElement) => string> = {
47
+ [KEYS.audio]: () => 'Audio',
48
+ [KEYS.blockquote]: () => 'Blockquote',
49
+ [KEYS.callout]: () => 'Callout',
50
+ [KEYS.codeBlock]: () => 'Code Block',
51
+ [KEYS.column]: () => 'Column',
52
+ [KEYS.equation]: () => 'Equation',
53
+ [KEYS.file]: () => 'File',
54
+ [KEYS.h1]: () => 'Heading 1',
55
+ [KEYS.h2]: () => 'Heading 2',
56
+ [KEYS.h3]: () => 'Heading 3',
57
+ [KEYS.h4]: () => 'Heading 4',
58
+ [KEYS.h5]: () => 'Heading 5',
59
+ [KEYS.h6]: () => 'Heading 6',
60
+ [KEYS.hr]: () => 'Horizontal Rule',
61
+ [KEYS.img]: () => 'Image',
62
+ [KEYS.mediaEmbed]: () => 'Media',
63
+ [KEYS.p]: (node) => {
64
+ if (node?.[KEYS.listType] === KEYS.listTodo) return 'Todo List';
65
+ if (node?.[KEYS.listType] === KEYS.ol) return 'Ordered List';
66
+ if (node?.[KEYS.listType] === KEYS.ul) return 'List';
67
+
68
+ return 'Paragraph';
69
+ },
70
+ [KEYS.table]: () => 'Table',
71
+ [KEYS.toc]: () => 'Table of Contents',
72
+ [KEYS.toggle]: () => 'Toggle',
73
+ [KEYS.video]: () => 'Video',
74
+ };
75
+
76
+ export function BlockSuggestionCard({
77
+ idx,
78
+ isLast,
79
+ suggestion,
80
+ }: {
81
+ idx: number;
82
+ isLast: boolean;
83
+ suggestion: ResolvedSuggestion;
84
+ }) {
85
+ const { api, editor } = useEditorPlugin(SuggestionPlugin);
86
+
87
+ const userInfo = usePluginOption(discussionPlugin, 'user', suggestion.userId);
88
+
89
+ const accept = (suggestion: ResolvedSuggestion) => {
90
+ api.suggestion.withoutSuggestions(() => {
91
+ acceptSuggestion(editor, suggestion);
92
+ });
93
+ };
94
+
95
+ const reject = (suggestion: ResolvedSuggestion) => {
96
+ api.suggestion.withoutSuggestions(() => {
97
+ rejectSuggestion(editor, suggestion);
98
+ });
99
+ };
100
+
101
+ const [hovering, setHovering] = React.useState(false);
102
+
103
+ const suggestionText2Array = (text: string) => {
104
+ if (text === BLOCK_SUGGESTION) return ['line breaks'];
105
+
106
+ return text.split(BLOCK_SUGGESTION).filter(Boolean);
107
+ };
108
+
109
+ const [editingId, setEditingId] = React.useState<string | null>(null);
110
+
111
+ return (
112
+ <div
113
+ key={`${suggestion.suggestionId}-${idx}`}
114
+ className="relative"
115
+ onMouseEnter={() => setHovering(true)}
116
+ onMouseLeave={() => setHovering(false)}
117
+ >
118
+ <div className="flex flex-col p-4">
119
+ <div className="relative flex items-center">
120
+ {/* Replace to your own backend or refer to potion */}
121
+ <Avatar className="size-5">
122
+ <AvatarImage alt={userInfo?.name} src={userInfo?.avatarUrl} />
123
+ <AvatarFallback>{userInfo?.name?.[0]}</AvatarFallback>
124
+ </Avatar>
125
+ <h4 className="mx-2 font-semibold text-sm leading-none">
126
+ {userInfo?.name}
127
+ </h4>
128
+ <div className="text-muted-foreground/80 text-xs leading-none">
129
+ <span className="mr-1">
130
+ {formatCommentDate(new Date(suggestion.createdAt))}
131
+ </span>
132
+ </div>
133
+ </div>
134
+
135
+ <div className="relative mt-1 mb-4 pl-[32px]">
136
+ <div className="flex flex-col gap-2">
137
+ {suggestion.type === 'remove' &&
138
+ suggestionText2Array(suggestion.text!).map((text, index) => (
139
+ <div key={index} className="flex items-center gap-2">
140
+ <span className="text-muted-foreground text-sm">Delete:</span>
141
+
142
+ <span key={index} className="text-sm">
143
+ {text}
144
+ </span>
145
+ </div>
146
+ ))}
147
+
148
+ {suggestion.type === 'insert' &&
149
+ suggestionText2Array(suggestion.newText!).map((text, index) => (
150
+ <div key={index} className="flex items-center gap-2">
151
+ <span className="text-muted-foreground text-sm">Add:</span>
152
+
153
+ <span key={index} className="text-sm">
154
+ {text || 'line breaks'}
155
+ </span>
156
+ </div>
157
+ ))}
158
+
159
+ {suggestion.type === 'replace' && (
160
+ <div className="flex flex-col gap-2">
161
+ {suggestionText2Array(suggestion.newText!).map(
162
+ (text, index) => (
163
+ <React.Fragment key={index}>
164
+ <div
165
+ key={index}
166
+ className="flex items-start gap-2 text-brand/80"
167
+ >
168
+ <span className="text-sm">with:</span>
169
+ <span className="text-sm">{text || 'line breaks'}</span>
170
+ </div>
171
+ </React.Fragment>
172
+ )
173
+ )}
174
+
175
+ {suggestionText2Array(suggestion.text!).map((text, index) => (
176
+ <React.Fragment key={index}>
177
+ <div key={index} className="flex items-start gap-2">
178
+ <span className="text-muted-foreground text-sm">
179
+ {index === 0 ? 'Replace:' : 'Delete:'}
180
+ </span>
181
+ <span className="text-sm">{text || 'line breaks'}</span>
182
+ </div>
183
+ </React.Fragment>
184
+ ))}
185
+ </div>
186
+ )}
187
+
188
+ {suggestion.type === 'update' && (
189
+ <div className="flex items-center gap-2">
190
+ <span className="text-muted-foreground text-sm">
191
+ {Object.keys(suggestion.properties).map((key) => (
192
+ <span key={key}>Un{key}</span>
193
+ ))}
194
+
195
+ {Object.keys(suggestion.newProperties).map((key) => (
196
+ <span key={key}>
197
+ {key.charAt(0).toUpperCase() + key.slice(1)}
198
+ </span>
199
+ ))}
200
+ </span>
201
+ <span className="text-sm">{suggestion.newText}</span>
202
+ </div>
203
+ )}
204
+ </div>
205
+ </div>
206
+
207
+ {suggestion.comments.map((comment, index) => (
208
+ <Comment
209
+ key={comment.id ?? index}
210
+ comment={comment}
211
+ discussionLength={suggestion.comments.length}
212
+ documentContent="__suggestion__"
213
+ editingId={editingId}
214
+ index={index}
215
+ setEditingId={setEditingId}
216
+ />
217
+ ))}
218
+
219
+ {hovering && (
220
+ <div className="absolute top-4 right-4 flex gap-2">
221
+ <Button
222
+ variant="ghost"
223
+ className="size-6 p-1 text-muted-foreground"
224
+ onClick={() => accept(suggestion)}
225
+ >
226
+ <CheckIcon className="size-4" />
227
+ </Button>
228
+
229
+ <Button
230
+ variant="ghost"
231
+ className="size-6 p-1 text-muted-foreground"
232
+ onClick={() => reject(suggestion)}
233
+ >
234
+ <XIcon className="size-4" />
235
+ </Button>
236
+ </div>
237
+ )}
238
+
239
+ <CommentCreateForm discussionId={suggestion.suggestionId} />
240
+ </div>
241
+
242
+ {!isLast && <div className="h-px w-full bg-muted" />}
243
+ </div>
244
+ );
245
+ }
246
+
247
+ export const useResolveSuggestion = (
248
+ suggestionNodes: NodeEntry<TElement | TSuggestionText>[],
249
+ blockPath: Path
250
+ ) => {
251
+ const discussions = usePluginOption(discussionPlugin, 'discussions');
252
+
253
+ const { api, editor, getOption, setOption } =
254
+ useEditorPlugin(suggestionPlugin);
255
+
256
+ suggestionNodes.forEach(([node]) => {
257
+ const id = api.suggestion.nodeId(node);
258
+ const map = getOption('uniquePathMap');
259
+
260
+ if (!id) return;
261
+
262
+ const previousPath = map.get(id);
263
+
264
+ // If there are no suggestion nodes in the corresponding path in the map, then update it.
265
+ if (PathApi.isPath(previousPath)) {
266
+ const nodes = api.suggestion.node({ id, at: previousPath, isText: true });
267
+ const parentNode = api.node(previousPath);
268
+ let lineBreakId: string | null = null;
269
+
270
+ if (parentNode && ElementApi.isElement(parentNode[0])) {
271
+ lineBreakId = api.suggestion.nodeId(parentNode[0]) ?? null;
272
+ }
273
+
274
+ if (!nodes && lineBreakId !== id) {
275
+ setOption('uniquePathMap', new Map(map).set(id, blockPath));
276
+ }
277
+ } else {
278
+ setOption('uniquePathMap', new Map(map).set(id, blockPath));
279
+ }
280
+ });
281
+
282
+ const resolvedSuggestion: ResolvedSuggestion[] = React.useMemo(() => {
283
+ const map = getOption('uniquePathMap');
284
+
285
+ if (suggestionNodes.length === 0) return [];
286
+
287
+ const suggestionIds = new Set(
288
+ suggestionNodes
289
+ .flatMap(([node]) => {
290
+ if (TextApi.isText(node)) {
291
+ const dataList = api.suggestion.dataList(node);
292
+ const includeUpdate = dataList.some(
293
+ (data) => data.type === 'update'
294
+ );
295
+
296
+ if (!includeUpdate) {
297
+ return api.suggestion.nodeId(node) ?? [];
298
+ }
299
+
300
+ return dataList
301
+ .filter((data) => data.type === 'update')
302
+ .map((d) => d.id);
303
+ }
304
+ if (ElementApi.isElement(node)) {
305
+ return api.suggestion.nodeId(node) ?? [];
306
+ }
307
+
308
+ return [];
309
+ })
310
+ .filter(Boolean)
311
+ );
312
+
313
+ const res: ResolvedSuggestion[] = [];
314
+
315
+ suggestionIds.forEach((id) => {
316
+ if (!id) return;
317
+
318
+ const path = map.get(id);
319
+
320
+ if (!path || !PathApi.isPath(path)) return;
321
+ if (!PathApi.equals(path, blockPath)) return;
322
+
323
+ const entries = [
324
+ ...editor.api.nodes<TElement | TSuggestionText>({
325
+ at: [],
326
+ mode: 'all',
327
+ match: (n) =>
328
+ (n[KEYS.suggestion] && n[getSuggestionKey(id)]) ||
329
+ api.suggestion.nodeId(n as TElement) === id,
330
+ }),
331
+ ];
332
+
333
+ // move line break to the end
334
+ entries.sort(([, path1], [, path2]) =>
335
+ PathApi.isChild(path1, path2) ? -1 : 1
336
+ );
337
+
338
+ let newText = '';
339
+ let text = '';
340
+ let properties: any = {};
341
+ let newProperties: any = {};
342
+
343
+ // overlapping suggestion
344
+ entries.forEach(([node]) => {
345
+ if (TextApi.isText(node)) {
346
+ const dataList = api.suggestion.dataList(node);
347
+
348
+ dataList.forEach((data) => {
349
+ if (data.id === id) {
350
+ switch (data.type) {
351
+ case 'insert': {
352
+ newText += node.text;
353
+
354
+ break;
355
+ }
356
+ case 'remove': {
357
+ text += node.text;
358
+
359
+ break;
360
+ }
361
+ case 'update': {
362
+ properties = {
363
+ ...properties,
364
+ ...data.properties,
365
+ };
366
+
367
+ newProperties = {
368
+ ...newProperties,
369
+ ...data.newProperties,
370
+ };
371
+
372
+ newText += node.text;
373
+
374
+ break;
375
+ }
376
+ // No default
377
+ }
378
+ }
379
+ });
380
+ } else {
381
+ const lineBreakData = api.suggestion.isBlockSuggestion(node)
382
+ ? node.suggestion
383
+ : undefined;
384
+
385
+ if (lineBreakData?.id === keyId2SuggestionId(id)) {
386
+ if (lineBreakData.type === 'insert') {
387
+ newText += lineBreakData.isLineBreak
388
+ ? BLOCK_SUGGESTION
389
+ : BLOCK_SUGGESTION + TYPE_TEXT_MAP[node.type](node);
390
+ } else if (lineBreakData.type === 'remove') {
391
+ text += lineBreakData.isLineBreak
392
+ ? BLOCK_SUGGESTION
393
+ : BLOCK_SUGGESTION + TYPE_TEXT_MAP[node.type](node);
394
+ }
395
+ }
396
+ }
397
+ });
398
+
399
+ if (entries.length === 0) return;
400
+
401
+ const nodeData = api.suggestion.suggestionData(entries[0][0]);
402
+
403
+ if (!nodeData) return;
404
+
405
+ // const comments = data?.discussions.find((d) => d.id === id)?.comments;
406
+ const comments =
407
+ discussions.find((s: TDiscussion) => s.id === id)?.comments || [];
408
+ const createdAt = new Date(nodeData.createdAt);
409
+
410
+ const keyId = getSuggestionKey(id);
411
+
412
+ if (nodeData.type === 'update') {
413
+ res.push({
414
+ comments,
415
+ createdAt,
416
+ keyId,
417
+ newProperties,
418
+ newText,
419
+ properties,
420
+ suggestionId: keyId2SuggestionId(id),
421
+ type: 'update',
422
+ userId: nodeData.userId,
423
+ });
424
+ } else if (newText.length > 0 && text.length > 0) {
425
+ res.push({
426
+ comments,
427
+ createdAt,
428
+ keyId,
429
+ newText,
430
+ suggestionId: keyId2SuggestionId(id),
431
+ text,
432
+ type: 'replace',
433
+ userId: nodeData.userId,
434
+ });
435
+ } else if (newText.length > 0) {
436
+ res.push({
437
+ comments,
438
+ createdAt,
439
+ keyId,
440
+ newText,
441
+ suggestionId: keyId2SuggestionId(id),
442
+ type: 'insert',
443
+ userId: nodeData.userId,
444
+ });
445
+ } else if (text.length > 0) {
446
+ res.push({
447
+ comments,
448
+ createdAt,
449
+ keyId,
450
+ suggestionId: keyId2SuggestionId(id),
451
+ text,
452
+ type: 'remove',
453
+ userId: nodeData.userId,
454
+ });
455
+ }
456
+ });
457
+
458
+ return res;
459
+ }, [
460
+ api.suggestion,
461
+ blockPath,
462
+ discussions,
463
+ editor.api,
464
+ getOption,
465
+ suggestionNodes,
466
+ ]);
467
+
468
+ return resolvedSuggestion;
469
+ };
470
+
471
+ export const isResolvedSuggestion = (
472
+ suggestion: ResolvedSuggestion | TDiscussion
473
+ ): suggestion is ResolvedSuggestion => 'suggestionId' in suggestion;
@@ -0,0 +1,11 @@
1
+ import { type SlateElementProps, SlateElement } from 'platejs/static';
2
+
3
+ export function BlockquoteElementStatic(props: SlateElementProps) {
4
+ return (
5
+ <SlateElement
6
+ as="blockquote"
7
+ className="my-1 border-l-2 pl-6 italic"
8
+ {...props}
9
+ />
10
+ );
11
+ }
@@ -0,0 +1,13 @@
1
+ 'use client';
2
+
3
+ import { type PlateElementProps, PlateElement } from 'platejs/react';
4
+
5
+ export function BlockquoteElement(props: PlateElementProps) {
6
+ return (
7
+ <PlateElement
8
+ as="blockquote"
9
+ className="my-1 border-l-2 pl-6 italic"
10
+ {...props}
11
+ />
12
+ );
13
+ }
@@ -0,0 +1,62 @@
1
+ import * as React from "react"
2
+ import { Slot } from "@radix-ui/react-slot"
3
+ import { cva, type VariantProps } from "class-variance-authority"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground hover:bg-primary/90",
13
+ destructive:
14
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
15
+ outline:
16
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
17
+ secondary:
18
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19
+ ghost:
20
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
21
+ link: "text-primary underline-offset-4 hover:underline",
22
+ },
23
+ size: {
24
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
25
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
26
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
27
+ icon: "size-9",
28
+ "icon-sm": "size-8",
29
+ "icon-lg": "size-10",
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ variant: "default",
34
+ size: "default",
35
+ },
36
+ }
37
+ )
38
+
39
+ function Button({
40
+ className,
41
+ variant = "default",
42
+ size = "default",
43
+ asChild = false,
44
+ ...props
45
+ }: React.ComponentProps<"button"> &
46
+ VariantProps<typeof buttonVariants> & {
47
+ asChild?: boolean
48
+ }) {
49
+ const Comp = asChild ? Slot : "button"
50
+
51
+ return (
52
+ <Comp
53
+ data-slot="button"
54
+ data-variant={variant}
55
+ data-size={size}
56
+ className={cn(buttonVariants({ variant, size, className }))}
57
+ {...props}
58
+ />
59
+ )
60
+ }
61
+
62
+ export { Button, buttonVariants }