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,512 @@
1
+ import React from 'react';
2
+
3
+ import { DndPlugin, useDraggable, useDropLine } from '@platejs/dnd';
4
+ import { expandListItemsWithChildren } from '@platejs/list';
5
+ import { BlockSelectionPlugin } from '@platejs/selection/react';
6
+ import { GripVertical } from 'lucide-react';
7
+ import { type TElement, getPluginByType, isType, KEYS } from 'platejs';
8
+ import {
9
+ type PlateEditor,
10
+ type PlateElementProps,
11
+ type RenderNodeWrapper,
12
+ MemoizedChildren,
13
+ useEditorRef,
14
+ useElement,
15
+ usePluginOption,
16
+ } from 'platejs/react';
17
+ import { useSelected } from 'platejs/react';
18
+
19
+ import { Button } from '@/components/ui/button';
20
+ import {
21
+ Tooltip,
22
+ TooltipContent,
23
+ TooltipTrigger,
24
+ } from '@/components/ui/tooltip';
25
+ import { cn } from '@/lib/utils';
26
+
27
+ const UNDRAGGABLE_KEYS = [KEYS.column, KEYS.tr, KEYS.td];
28
+
29
+ export const BlockDraggable: RenderNodeWrapper = (props) => {
30
+ const { editor, element, path } = props;
31
+
32
+ const enabled = React.useMemo(() => {
33
+ if (editor.dom.readOnly) return false;
34
+
35
+ if (path.length === 1 && !isType(editor, element, UNDRAGGABLE_KEYS)) {
36
+ return true;
37
+ }
38
+ if (path.length === 3 && !isType(editor, element, UNDRAGGABLE_KEYS)) {
39
+ const block = editor.api.some({
40
+ at: path,
41
+ match: {
42
+ type: editor.getType(KEYS.column),
43
+ },
44
+ });
45
+
46
+ if (block) {
47
+ return true;
48
+ }
49
+ }
50
+ if (path.length === 4 && !isType(editor, element, UNDRAGGABLE_KEYS)) {
51
+ const block = editor.api.some({
52
+ at: path,
53
+ match: {
54
+ type: editor.getType(KEYS.table),
55
+ },
56
+ });
57
+
58
+ if (block) {
59
+ return true;
60
+ }
61
+ }
62
+
63
+ return false;
64
+ }, [editor, element, path]);
65
+
66
+ if (!enabled) return;
67
+
68
+ return (props) => <Draggable {...props} />;
69
+ };
70
+
71
+ function Draggable(props: PlateElementProps) {
72
+ const { children, editor, element, path } = props;
73
+ const blockSelectionApi = editor.getApi(BlockSelectionPlugin).blockSelection;
74
+
75
+ const { isAboutToDrag, isDragging, nodeRef, previewRef, handleRef } =
76
+ useDraggable({
77
+ element,
78
+ onDropHandler: (_, { dragItem }) => {
79
+ const id = (dragItem as { id: string[] | string }).id;
80
+
81
+ if (blockSelectionApi) {
82
+ blockSelectionApi.add(id);
83
+ }
84
+ resetPreview();
85
+ },
86
+ });
87
+
88
+ const isInColumn = path.length === 3;
89
+ const isInTable = path.length === 4;
90
+
91
+ const [previewTop, setPreviewTop] = React.useState(0);
92
+
93
+ const resetPreview = () => {
94
+ if (previewRef.current) {
95
+ previewRef.current.replaceChildren();
96
+ previewRef.current?.classList.add('hidden');
97
+ }
98
+ };
99
+
100
+ // clear up virtual multiple preview when drag end
101
+ React.useEffect(() => {
102
+ if (!isDragging) {
103
+ resetPreview();
104
+ }
105
+ // eslint-disable-next-line react-hooks/exhaustive-deps
106
+ }, [isDragging]);
107
+
108
+ React.useEffect(() => {
109
+ if (isAboutToDrag) {
110
+ previewRef.current?.classList.remove('opacity-0');
111
+ }
112
+ // eslint-disable-next-line react-hooks/exhaustive-deps
113
+ }, [isAboutToDrag]);
114
+
115
+ const [dragButtonTop, setDragButtonTop] = React.useState(0);
116
+
117
+ return (
118
+ <div
119
+ className={cn(
120
+ 'relative',
121
+ isDragging && 'opacity-50',
122
+ getPluginByType(editor, element.type)?.node.isContainer
123
+ ? 'group/container'
124
+ : 'group'
125
+ )}
126
+ onMouseEnter={() => {
127
+ if (isDragging) return;
128
+ setDragButtonTop(calcDragButtonTop(editor, element));
129
+ }}
130
+ >
131
+ {!isInTable && (
132
+ <Gutter>
133
+ <div
134
+ className={cn(
135
+ 'slate-blockToolbarWrapper',
136
+ 'flex h-[1.5em]',
137
+ isInColumn && 'h-4'
138
+ )}
139
+ >
140
+ <div
141
+ className={cn(
142
+ 'slate-blockToolbar relative w-4.5',
143
+ 'pointer-events-auto mr-1 flex items-center',
144
+ isInColumn && 'mr-1.5'
145
+ )}
146
+ >
147
+ <Button
148
+ ref={handleRef}
149
+ variant="ghost"
150
+ className="-left-0 absolute h-6 w-full p-0"
151
+ style={{ top: `${dragButtonTop + 3}px` }}
152
+ data-plate-prevent-deselect
153
+ >
154
+ <DragHandle
155
+ isDragging={isDragging}
156
+ previewRef={previewRef}
157
+ resetPreview={resetPreview}
158
+ setPreviewTop={setPreviewTop}
159
+ />
160
+ </Button>
161
+ </div>
162
+ </div>
163
+ </Gutter>
164
+ )}
165
+
166
+ <div
167
+ ref={previewRef}
168
+ className={cn('-left-0 absolute hidden w-full')}
169
+ style={{ top: `${-previewTop}px` }}
170
+ contentEditable={false}
171
+ />
172
+
173
+ <div
174
+ ref={nodeRef}
175
+ className="slate-blockWrapper flow-root"
176
+ onContextMenu={(event) =>
177
+ editor
178
+ .getApi(BlockSelectionPlugin)
179
+ .blockSelection.addOnContextMenu({ element, event })
180
+ }
181
+ >
182
+ <MemoizedChildren>{children}</MemoizedChildren>
183
+ <DropLine />
184
+ </div>
185
+ </div>
186
+ );
187
+ }
188
+
189
+ function Gutter({
190
+ children,
191
+ className,
192
+ ...props
193
+ }: React.ComponentProps<'div'>) {
194
+ const editor = useEditorRef();
195
+ const element = useElement();
196
+ const isSelectionAreaVisible = usePluginOption(
197
+ BlockSelectionPlugin,
198
+ 'isSelectionAreaVisible'
199
+ );
200
+ const selected = useSelected();
201
+
202
+ return (
203
+ <div
204
+ {...props}
205
+ className={cn(
206
+ 'slate-gutterLeft',
207
+ '-translate-x-full absolute top-0 z-50 flex h-full cursor-text hover:opacity-100 sm:opacity-0',
208
+ getPluginByType(editor, element.type)?.node.isContainer
209
+ ? 'group-hover/container:opacity-100'
210
+ : 'group-hover:opacity-100',
211
+ isSelectionAreaVisible && 'hidden',
212
+ !selected && 'opacity-0',
213
+ className
214
+ )}
215
+ contentEditable={false}
216
+ >
217
+ {children}
218
+ </div>
219
+ );
220
+ }
221
+
222
+ const DragHandle = React.memo(function DragHandle({
223
+ isDragging,
224
+ previewRef,
225
+ resetPreview,
226
+ setPreviewTop,
227
+ }: {
228
+ isDragging: boolean;
229
+ previewRef: React.RefObject<HTMLDivElement | null>;
230
+ resetPreview: () => void;
231
+ setPreviewTop: (top: number) => void;
232
+ }) {
233
+ const editor = useEditorRef();
234
+ const element = useElement();
235
+
236
+ return (
237
+ <Tooltip>
238
+ <TooltipTrigger asChild>
239
+ <div
240
+ className="flex size-full items-center justify-center"
241
+ onClick={(e) => {
242
+ e.preventDefault();
243
+ editor.getApi(BlockSelectionPlugin).blockSelection.focus();
244
+ }}
245
+ onMouseDown={(e) => {
246
+ resetPreview();
247
+
248
+ if ((e.button !== 0 && e.button !== 2) || e.shiftKey) return;
249
+
250
+ const blockSelection = editor
251
+ .getApi(BlockSelectionPlugin)
252
+ .blockSelection.getNodes({ sort: true });
253
+
254
+ let selectionNodes =
255
+ blockSelection.length > 0
256
+ ? blockSelection
257
+ : editor.api.blocks({ mode: 'highest' });
258
+
259
+ // If current block is not in selection, use it as the starting point
260
+ if (!selectionNodes.some(([node]) => node.id === element.id)) {
261
+ selectionNodes = [[element, editor.api.findPath(element)!]];
262
+ }
263
+
264
+ // Process selection nodes to include list children
265
+ const blocks = expandListItemsWithChildren(
266
+ editor,
267
+ selectionNodes
268
+ ).map(([node]) => node);
269
+
270
+ if (blockSelection.length === 0) {
271
+ editor.tf.blur();
272
+ editor.tf.collapse();
273
+ }
274
+
275
+ const elements = createDragPreviewElements(editor, blocks);
276
+ previewRef.current?.append(...elements);
277
+ previewRef.current?.classList.remove('hidden');
278
+ previewRef.current?.classList.add('opacity-0');
279
+ editor.setOption(DndPlugin, 'multiplePreviewRef', previewRef);
280
+
281
+ editor
282
+ .getApi(BlockSelectionPlugin)
283
+ .blockSelection.set(blocks.map((block) => block.id as string));
284
+ }}
285
+ onMouseEnter={() => {
286
+ if (isDragging) return;
287
+
288
+ const blockSelection = editor
289
+ .getApi(BlockSelectionPlugin)
290
+ .blockSelection.getNodes({ sort: true });
291
+
292
+ let selectedBlocks =
293
+ blockSelection.length > 0
294
+ ? blockSelection
295
+ : editor.api.blocks({ mode: 'highest' });
296
+
297
+ // If current block is not in selection, use it as the starting point
298
+ if (!selectedBlocks.some(([node]) => node.id === element.id)) {
299
+ selectedBlocks = [[element, editor.api.findPath(element)!]];
300
+ }
301
+
302
+ // Process selection to include list children
303
+ const processedBlocks = expandListItemsWithChildren(
304
+ editor,
305
+ selectedBlocks
306
+ );
307
+
308
+ const ids = processedBlocks.map((block) => block[0].id as string);
309
+
310
+ if (ids.length > 1 && ids.includes(element.id as string)) {
311
+ const previewTop = calculatePreviewTop(editor, {
312
+ blocks: processedBlocks.map((block) => block[0]),
313
+ element,
314
+ });
315
+ setPreviewTop(previewTop);
316
+ } else {
317
+ setPreviewTop(0);
318
+ }
319
+ }}
320
+ onMouseUp={() => {
321
+ resetPreview();
322
+ }}
323
+ data-plate-prevent-deselect
324
+ role="button"
325
+ >
326
+ <GripVertical className="text-muted-foreground" />
327
+ </div>
328
+ </TooltipTrigger>
329
+ <TooltipContent>Drag to move</TooltipContent>
330
+ </Tooltip>
331
+ );
332
+ });
333
+
334
+ const DropLine = React.memo(function DropLine({
335
+ className,
336
+ ...props
337
+ }: React.ComponentProps<'div'>) {
338
+ const { dropLine } = useDropLine();
339
+
340
+ if (!dropLine) return null;
341
+
342
+ return (
343
+ <div
344
+ {...props}
345
+ className={cn(
346
+ 'slate-dropLine',
347
+ 'absolute inset-x-0 h-0.5 opacity-100 transition-opacity',
348
+ 'bg-brand/50',
349
+ dropLine === 'top' && '-top-px',
350
+ dropLine === 'bottom' && '-bottom-px',
351
+ className
352
+ )}
353
+ />
354
+ );
355
+ });
356
+
357
+ const createDragPreviewElements = (
358
+ editor: PlateEditor,
359
+ blocks: TElement[]
360
+ ): HTMLElement[] => {
361
+ const elements: HTMLElement[] = [];
362
+ const ids: string[] = [];
363
+
364
+ /**
365
+ * Remove data attributes from the element to avoid recognized as slate
366
+ * elements incorrectly.
367
+ */
368
+ const removeDataAttributes = (element: HTMLElement) => {
369
+ Array.from(element.attributes).forEach((attr) => {
370
+ if (
371
+ attr.name.startsWith('data-slate') ||
372
+ attr.name.startsWith('data-block-id')
373
+ ) {
374
+ element.removeAttribute(attr.name);
375
+ }
376
+ });
377
+
378
+ Array.from(element.children).forEach((child) => {
379
+ removeDataAttributes(child as HTMLElement);
380
+ });
381
+ };
382
+
383
+ const resolveElement = (node: TElement, index: number) => {
384
+ const domNode = editor.api.toDOMNode(node)!;
385
+ const newDomNode = domNode.cloneNode(true) as HTMLElement;
386
+
387
+ // Apply visual compensation for horizontal scroll
388
+ const applyScrollCompensation = (
389
+ original: Element,
390
+ cloned: HTMLElement
391
+ ) => {
392
+ const scrollLeft = original.scrollLeft;
393
+
394
+ if (scrollLeft > 0) {
395
+ // Create a wrapper to handle the scroll offset
396
+ const scrollWrapper = document.createElement('div');
397
+ scrollWrapper.style.overflow = 'hidden';
398
+ scrollWrapper.style.width = `${original.clientWidth}px`;
399
+
400
+ // Create inner container with the full content
401
+ const innerContainer = document.createElement('div');
402
+ innerContainer.style.transform = `translateX(-${scrollLeft}px)`;
403
+ innerContainer.style.width = `${original.scrollWidth}px`;
404
+
405
+ // Move all children to the inner container
406
+ while (cloned.firstChild) {
407
+ innerContainer.append(cloned.firstChild);
408
+ }
409
+
410
+ // Apply the original element's styles to maintain appearance
411
+ const originalStyles = window.getComputedStyle(original);
412
+ cloned.style.padding = '0';
413
+ innerContainer.style.padding = originalStyles.padding;
414
+
415
+ scrollWrapper.append(innerContainer);
416
+ cloned.append(scrollWrapper);
417
+ }
418
+ };
419
+
420
+ applyScrollCompensation(domNode, newDomNode);
421
+
422
+ ids.push(node.id as string);
423
+ const wrapper = document.createElement('div');
424
+ wrapper.append(newDomNode);
425
+ wrapper.style.display = 'flow-root';
426
+
427
+ const lastDomNode = blocks[index - 1];
428
+
429
+ if (lastDomNode) {
430
+ const lastDomNodeRect = editor.api
431
+ .toDOMNode(lastDomNode)!
432
+ .parentElement!.getBoundingClientRect();
433
+
434
+ const domNodeRect = domNode.parentElement!.getBoundingClientRect();
435
+
436
+ const distance = domNodeRect.top - lastDomNodeRect.bottom;
437
+
438
+ // Check if the two elements are adjacent (touching each other)
439
+ if (distance > 15) {
440
+ wrapper.style.marginTop = `${distance}px`;
441
+ }
442
+ }
443
+
444
+ removeDataAttributes(newDomNode);
445
+ elements.push(wrapper);
446
+ };
447
+
448
+ blocks.forEach((node, index) => {
449
+ resolveElement(node, index);
450
+ });
451
+
452
+ editor.setOption(DndPlugin, 'draggingId', ids);
453
+
454
+ return elements;
455
+ };
456
+
457
+ const calculatePreviewTop = (
458
+ editor: PlateEditor,
459
+ {
460
+ blocks,
461
+ element,
462
+ }: {
463
+ blocks: TElement[];
464
+ element: TElement;
465
+ }
466
+ ): number => {
467
+ const child = editor.api.toDOMNode(element)!;
468
+ const editable = editor.api.toDOMNode(editor)!;
469
+ const firstSelectedChild = blocks[0];
470
+
471
+ const firstDomNode = editor.api.toDOMNode(firstSelectedChild)!;
472
+ // Get editor's top padding
473
+ const editorPaddingTop = Number(
474
+ window.getComputedStyle(editable).paddingTop.replace('px', '')
475
+ );
476
+
477
+ // Calculate distance from first selected node to editor top
478
+ const firstNodeToEditorDistance =
479
+ firstDomNode.getBoundingClientRect().top -
480
+ editable.getBoundingClientRect().top -
481
+ editorPaddingTop;
482
+
483
+ // Get margin top of first selected node
484
+ const firstMarginTopString = window.getComputedStyle(firstDomNode).marginTop;
485
+ const marginTop = Number(firstMarginTopString.replace('px', ''));
486
+
487
+ // Calculate distance from current node to editor top
488
+ const currentToEditorDistance =
489
+ child.getBoundingClientRect().top -
490
+ editable.getBoundingClientRect().top -
491
+ editorPaddingTop;
492
+
493
+ const currentMarginTopString = window.getComputedStyle(child).marginTop;
494
+ const currentMarginTop = Number(currentMarginTopString.replace('px', ''));
495
+
496
+ const previewElementsTopDistance =
497
+ currentToEditorDistance -
498
+ firstNodeToEditorDistance +
499
+ marginTop -
500
+ currentMarginTop;
501
+
502
+ return previewElementsTopDistance;
503
+ };
504
+
505
+ const calcDragButtonTop = (editor: PlateEditor, element: TElement): number => {
506
+ const child = editor.api.toDOMNode(element)!;
507
+
508
+ const currentMarginTopString = window.getComputedStyle(child).marginTop;
509
+ const currentMarginTop = Number(currentMarginTopString.replace('px', ''));
510
+
511
+ return currentMarginTop;
512
+ };
@@ -0,0 +1,80 @@
1
+ import * as React from 'react';
2
+
3
+ import type { RenderStaticNodeWrapper, TListElement } from 'platejs';
4
+ import type { SlateRenderElementProps } from 'platejs/static';
5
+
6
+ import { isOrderedList } from '@platejs/list';
7
+ import { CheckIcon } from 'lucide-react';
8
+
9
+ import { cn } from '@/lib/utils';
10
+
11
+ const config: Record<
12
+ string,
13
+ {
14
+ Li: React.FC<SlateRenderElementProps>;
15
+ Marker: React.FC<SlateRenderElementProps>;
16
+ }
17
+ > = {
18
+ todo: {
19
+ Li: TodoLiStatic,
20
+ Marker: TodoMarkerStatic,
21
+ },
22
+ };
23
+
24
+ export const BlockListStatic: RenderStaticNodeWrapper = (props) => {
25
+ if (!props.element.listStyleType) return;
26
+
27
+ return (props) => <List {...props} />;
28
+ };
29
+
30
+ function List(props: SlateRenderElementProps) {
31
+ const { listStart, listStyleType } = props.element as TListElement;
32
+ const { Li, Marker } = config[listStyleType] ?? {};
33
+ const List = isOrderedList(props.element) ? 'ol' : 'ul';
34
+
35
+ return (
36
+ <List
37
+ className="relative m-0 p-0"
38
+ style={{ listStyleType }}
39
+ start={listStart}
40
+ >
41
+ {Marker && <Marker {...props} />}
42
+ {Li ? <Li {...props} /> : <li>{props.children}</li>}
43
+ </List>
44
+ );
45
+ }
46
+
47
+ function TodoMarkerStatic(props: SlateRenderElementProps) {
48
+ const checked = props.element.checked as boolean;
49
+
50
+ return (
51
+ <div contentEditable={false}>
52
+ <button
53
+ className={cn(
54
+ 'peer -left-6 pointer-events-none absolute top-1 size-4 shrink-0 rounded-sm border border-primary bg-background ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground',
55
+ props.className
56
+ )}
57
+ data-state={checked ? 'checked' : 'unchecked'}
58
+ type="button"
59
+ >
60
+ <div className={cn('flex items-center justify-center text-current')}>
61
+ {checked && <CheckIcon className="size-4" />}
62
+ </div>
63
+ </button>
64
+ </div>
65
+ );
66
+ }
67
+
68
+ function TodoLiStatic(props: SlateRenderElementProps) {
69
+ return (
70
+ <li
71
+ className={cn(
72
+ 'list-none',
73
+ (props.element.checked as boolean) &&
74
+ 'text-muted-foreground line-through'
75
+ )}
76
+ >
77
+ {props.children}
78
+ </li>
79
+ );
80
+ }
@@ -0,0 +1,87 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ import type { TListElement } from 'platejs';
6
+
7
+ import { isOrderedList } from '@platejs/list';
8
+ import {
9
+ useTodoListElement,
10
+ useTodoListElementState,
11
+ } from '@platejs/list/react';
12
+ import {
13
+ type PlateElementProps,
14
+ type RenderNodeWrapper,
15
+ useReadOnly,
16
+ } from 'platejs/react';
17
+
18
+ import { Checkbox } from '@/components/ui/checkbox';
19
+ import { cn } from '@/lib/utils';
20
+
21
+ const config: Record<
22
+ string,
23
+ {
24
+ Li: React.FC<PlateElementProps>;
25
+ Marker: React.FC<PlateElementProps>;
26
+ }
27
+ > = {
28
+ todo: {
29
+ Li: TodoLi,
30
+ Marker: TodoMarker,
31
+ },
32
+ };
33
+
34
+ export const BlockList: RenderNodeWrapper = (props) => {
35
+ if (!props.element.listStyleType) return;
36
+
37
+ return (props) => <List {...props} />;
38
+ };
39
+
40
+ function List(props: PlateElementProps) {
41
+ const { listStart, listStyleType } = props.element as TListElement;
42
+ const { Li, Marker } = config[listStyleType] ?? {};
43
+ const List = isOrderedList(props.element) ? 'ol' : 'ul';
44
+
45
+ return (
46
+ <List
47
+ className="relative m-0 p-0"
48
+ style={{ listStyleType }}
49
+ start={listStart}
50
+ >
51
+ {Marker && <Marker {...props} />}
52
+ {Li ? <Li {...props} /> : <li>{props.children}</li>}
53
+ </List>
54
+ );
55
+ }
56
+
57
+ function TodoMarker(props: PlateElementProps) {
58
+ const state = useTodoListElementState({ element: props.element });
59
+ const { checkboxProps } = useTodoListElement(state);
60
+ const readOnly = useReadOnly();
61
+
62
+ return (
63
+ <div contentEditable={false}>
64
+ <Checkbox
65
+ className={cn(
66
+ '-left-6 absolute top-1',
67
+ readOnly && 'pointer-events-none'
68
+ )}
69
+ {...checkboxProps}
70
+ />
71
+ </div>
72
+ );
73
+ }
74
+
75
+ function TodoLi(props: PlateElementProps) {
76
+ return (
77
+ <li
78
+ className={cn(
79
+ 'list-none',
80
+ (props.element.checked as boolean) &&
81
+ 'text-muted-foreground line-through'
82
+ )}
83
+ >
84
+ {props.children}
85
+ </li>
86
+ );
87
+ }