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,136 @@
1
+
2
+ import LiteYouTubeEmbed from 'react-lite-youtube-embed';
3
+ import { Tweet } from 'react-tweet';
4
+
5
+ import type { TMediaEmbedElement } from 'platejs';
6
+ import type { PlateElementProps } from 'platejs/react';
7
+
8
+ import { parseTwitterUrl, parseVideoUrl } from '@platejs/media';
9
+ import { MediaEmbedPlugin, useMediaState } from '@platejs/media/react';
10
+ import { ResizableProvider, useResizableValue } from '@platejs/resizable';
11
+ import { PlateElement, withHOC } from 'platejs/react';
12
+
13
+ import { cn } from '@/lib/utils';
14
+
15
+ import { Caption, CaptionTextarea } from './caption';
16
+ import { MediaToolbar } from './media-toolbar';
17
+ import {
18
+ mediaResizeHandleVariants,
19
+ Resizable,
20
+ ResizeHandle,
21
+ } from './resize-handle';
22
+
23
+ export const MediaEmbedElement = withHOC(
24
+ ResizableProvider,
25
+ function MediaEmbedElement(props: PlateElementProps<TMediaEmbedElement>) {
26
+ const {
27
+ align = 'center',
28
+ embed,
29
+ focused,
30
+ isTweet,
31
+ isVideo,
32
+ isYoutube,
33
+ readOnly,
34
+ selected,
35
+ } = useMediaState({
36
+ urlParsers: [parseTwitterUrl, parseVideoUrl],
37
+ });
38
+ const width = useResizableValue('width');
39
+ const provider = embed?.provider;
40
+
41
+ return (
42
+ <MediaToolbar plugin={MediaEmbedPlugin}>
43
+ <PlateElement className="py-2.5" {...props}>
44
+ <figure
45
+ className="group relative m-0 w-full cursor-default"
46
+ contentEditable={false}
47
+ >
48
+ <Resizable
49
+ align={align}
50
+ options={{
51
+ align,
52
+ maxWidth: isTweet ? 550 : '100%',
53
+ minWidth: isTweet ? 300 : 100,
54
+ }}
55
+ >
56
+ <ResizeHandle
57
+ className={mediaResizeHandleVariants({ direction: 'left' })}
58
+ options={{ direction: 'left' }}
59
+ />
60
+
61
+ {isVideo ? (
62
+ isYoutube ? (
63
+ <LiteYouTubeEmbed
64
+ id={embed!.id!}
65
+ title="youtube"
66
+ wrapperClass={cn(
67
+ 'rounded-sm',
68
+ focused && selected && 'ring-2 ring-ring ring-offset-2',
69
+ 'relative block cursor-pointer bg-black bg-center bg-cover [contain:content]',
70
+ '[&.lyt-activated]:before:absolute [&.lyt-activated]:before:top-0 [&.lyt-activated]:before:h-[60px] [&.lyt-activated]:before:w-full [&.lyt-activated]:before:bg-top [&.lyt-activated]:before:bg-repeat-x [&.lyt-activated]:before:pb-[50px] [&.lyt-activated]:before:[transition:all_0.2s_cubic-bezier(0,_0,_0.2,_1)]',
71
+ '[&.lyt-activated]:before:bg-[url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==)]',
72
+ 'after:block after:pb-[var(--aspect-ratio)] after:content-[""]',
73
+ '[&_>_iframe]:absolute [&_>_iframe]:top-0 [&_>_iframe]:left-0 [&_>_iframe]:size-full',
74
+ '[&_>_.lty-playbtn]:z-1 [&_>_.lty-playbtn]:h-[46px] [&_>_.lty-playbtn]:w-[70px] [&_>_.lty-playbtn]:rounded-[14%] [&_>_.lty-playbtn]:bg-[#212121] [&_>_.lty-playbtn]:opacity-80 [&_>_.lty-playbtn]:[transition:all_0.2s_cubic-bezier(0,_0,_0.2,_1)]',
75
+ '[&:hover_>_.lty-playbtn]:bg-[red] [&:hover_>_.lty-playbtn]:opacity-100',
76
+ '[&_>_.lty-playbtn]:before:border-[transparent_transparent_transparent_#fff] [&_>_.lty-playbtn]:before:border-y-[11px] [&_>_.lty-playbtn]:before:border-r-0 [&_>_.lty-playbtn]:before:border-l-[19px] [&_>_.lty-playbtn]:before:content-[""]',
77
+ '[&_>_.lty-playbtn]:absolute [&_>_.lty-playbtn]:top-1/2 [&_>_.lty-playbtn]:left-1/2 [&_>_.lty-playbtn]:[transform:translate3d(-50%,-50%,0)]',
78
+ '[&_>_.lty-playbtn]:before:absolute [&_>_.lty-playbtn]:before:top-1/2 [&_>_.lty-playbtn]:before:left-1/2 [&_>_.lty-playbtn]:before:[transform:translate3d(-50%,-50%,0)]',
79
+ '[&.lyt-activated]:cursor-[unset]',
80
+ '[&.lyt-activated]:before:pointer-events-none [&.lyt-activated]:before:opacity-0',
81
+ '[&.lyt-activated_>_.lty-playbtn]:pointer-events-none [&.lyt-activated_>_.lty-playbtn]:opacity-0!'
82
+ )}
83
+ />
84
+ ) : (
85
+ <div
86
+ className={cn(
87
+ provider === 'vimeo' && 'pb-[75%]',
88
+ provider === 'youku' && 'pb-[56.25%]',
89
+ provider === 'dailymotion' && 'pb-[56.0417%]',
90
+ provider === 'coub' && 'pb-[51.25%]'
91
+ )}
92
+ >
93
+ <iframe
94
+ className={cn(
95
+ 'absolute top-0 left-0 size-full rounded-sm',
96
+ isVideo && 'border-0',
97
+ focused && selected && 'ring-2 ring-ring ring-offset-2'
98
+ )}
99
+ title="embed"
100
+ src={embed!.url}
101
+ allowFullScreen
102
+ />
103
+ </div>
104
+ )
105
+ ) : null}
106
+
107
+ {isTweet && (
108
+ <div
109
+ className={cn(
110
+ '[&_.react-tweet-theme]:my-0',
111
+ !readOnly &&
112
+ selected &&
113
+ '[&_.react-tweet-theme]:ring-2 [&_.react-tweet-theme]:ring-ring [&_.react-tweet-theme]:ring-offset-2'
114
+ )}
115
+ >
116
+ <Tweet id={embed!.id!} />
117
+ </div>
118
+ )}
119
+
120
+ <ResizeHandle
121
+ className={mediaResizeHandleVariants({ direction: 'right' })}
122
+ options={{ direction: 'right' }}
123
+ />
124
+ </Resizable>
125
+
126
+ <Caption style={{ width }} align={align}>
127
+ <CaptionTextarea placeholder="Write a caption..." />
128
+ </Caption>
129
+ </figure>
130
+
131
+ {props.children}
132
+ </PlateElement>
133
+ </MediaToolbar>
134
+ );
135
+ }
136
+ );
@@ -0,0 +1,29 @@
1
+ import type { TFileElement } from 'platejs';
2
+ import type { SlateElementProps } from 'platejs/static';
3
+
4
+ import { FileUp } from 'lucide-react';
5
+ import { SlateElement } from 'platejs/static';
6
+
7
+ export function FileElementStatic(props: SlateElementProps<TFileElement>) {
8
+ const { name, url } = props.element;
9
+
10
+ return (
11
+ <SlateElement className="my-px rounded-sm" {...props}>
12
+ <a
13
+ className="group relative m-0 flex cursor-pointer items-center rounded px-0.5 py-[3px] hover:bg-muted"
14
+ contentEditable={false}
15
+ download={name}
16
+ href={url}
17
+ rel="noopener noreferrer"
18
+ role="button"
19
+ target="_blank"
20
+ >
21
+ <div className="flex items-center gap-1 p-1">
22
+ <FileUp className="size-5" />
23
+ <div>{name}</div>
24
+ </div>
25
+ </a>
26
+ {props.children}
27
+ </SlateElement>
28
+ );
29
+ }
@@ -0,0 +1,47 @@
1
+
2
+
3
+ import type { TFileElement } from 'platejs';
4
+ import type { PlateElementProps } from 'platejs/react';
5
+
6
+ import { useMediaState } from '@platejs/media/react';
7
+ import { ResizableProvider } from '@platejs/resizable';
8
+ import { FileUp } from 'lucide-react';
9
+ import { PlateElement, useReadOnly, withHOC } from 'platejs/react';
10
+
11
+ import { Caption, CaptionTextarea } from './caption';
12
+
13
+ export const FileElement = withHOC(
14
+ ResizableProvider,
15
+ function FileElement(props: PlateElementProps<TFileElement>) {
16
+ const readOnly = useReadOnly();
17
+ const { name, unsafeUrl } = useMediaState();
18
+
19
+ return (
20
+ <PlateElement className="my-px rounded-sm" {...props}>
21
+ <a
22
+ className="group relative m-0 flex cursor-pointer items-center rounded px-0.5 py-[3px] hover:bg-muted"
23
+ contentEditable={false}
24
+ download={name}
25
+ href={unsafeUrl}
26
+ rel="noopener noreferrer"
27
+ role="button"
28
+ target="_blank"
29
+ >
30
+ <div className="flex items-center gap-1 p-1">
31
+ <FileUp className="size-5" />
32
+ <div>{name}</div>
33
+ </div>
34
+
35
+ <Caption align="left">
36
+ <CaptionTextarea
37
+ className="text-left"
38
+ readOnly={readOnly}
39
+ placeholder="Write a caption..."
40
+ />
41
+ </Caption>
42
+ </a>
43
+ {props.children}
44
+ </PlateElement>
45
+ );
46
+ }
47
+ );
@@ -0,0 +1,39 @@
1
+ import type { TCaptionProps, TImageElement, TResizableProps } from 'platejs';
2
+ import type { SlateElementProps } from 'platejs/static';
3
+
4
+ import { NodeApi } from 'platejs';
5
+ import { SlateElement } from 'platejs/static';
6
+
7
+ import { cn } from '@/lib/utils';
8
+
9
+ export function ImageElementStatic(
10
+ props: SlateElementProps<TImageElement & TCaptionProps & TResizableProps>
11
+ ) {
12
+ const { align = 'center', caption, url, width } = props.element;
13
+
14
+ return (
15
+ <SlateElement {...props} className="py-2.5">
16
+ <figure className="group relative m-0 inline-block" style={{ width }}>
17
+ <div
18
+ className="relative min-w-[92px] max-w-full"
19
+ style={{ textAlign: align }}
20
+ >
21
+ <img
22
+ className={cn(
23
+ 'w-full max-w-full cursor-default object-cover px-0',
24
+ 'rounded-sm'
25
+ )}
26
+ alt={(props.attributes as any).alt}
27
+ src={url}
28
+ />
29
+ {caption && (
30
+ <figcaption className="mx-auto mt-2 h-[24px] max-w-full">
31
+ {NodeApi.string(caption[0])}
32
+ </figcaption>
33
+ )}
34
+ </div>
35
+ </figure>
36
+ {props.children}
37
+ </SlateElement>
38
+ );
39
+ }
@@ -0,0 +1,80 @@
1
+
2
+
3
+ import type { TImageElement } from 'platejs';
4
+ import type { PlateElementProps } from 'platejs/react';
5
+
6
+ import { useDraggable } from '@platejs/dnd';
7
+ import { Image, ImagePlugin, useMediaState } from '@platejs/media/react';
8
+ import { ResizableProvider, useResizableValue } from '@platejs/resizable';
9
+ import { PlateElement, withHOC } from 'platejs/react';
10
+
11
+ import { cn } from '@/lib/utils';
12
+
13
+ import { Caption, CaptionTextarea } from './caption';
14
+ import { MediaToolbar } from './media-toolbar';
15
+ import {
16
+ mediaResizeHandleVariants,
17
+ Resizable,
18
+ ResizeHandle,
19
+ } from './resize-handle';
20
+
21
+ export const ImageElement = withHOC(
22
+ ResizableProvider,
23
+ function ImageElement(props: PlateElementProps<TImageElement>) {
24
+ const { align = 'center', focused, readOnly, selected } = useMediaState();
25
+ const width = useResizableValue('width');
26
+
27
+ const { isDragging, handleRef } = useDraggable({
28
+ element: props.element,
29
+ });
30
+
31
+ return (
32
+ <MediaToolbar plugin={ImagePlugin}>
33
+ <PlateElement {...props} className="py-2.5">
34
+ <figure className="group relative m-0" contentEditable={false}>
35
+ <Resizable
36
+ align={align}
37
+ options={{
38
+ align,
39
+ readOnly,
40
+ }}
41
+ >
42
+ <ResizeHandle
43
+ className={mediaResizeHandleVariants({ direction: 'left' })}
44
+ options={{ direction: 'left' }}
45
+ />
46
+ <Image
47
+ ref={handleRef}
48
+ className={cn(
49
+ 'block w-full max-w-full cursor-pointer object-cover px-0',
50
+ 'rounded-sm',
51
+ focused && selected && 'ring-2 ring-ring ring-offset-2',
52
+ isDragging && 'opacity-50'
53
+ )}
54
+ alt={props.attributes.alt as string | undefined}
55
+ />
56
+ <ResizeHandle
57
+ className={mediaResizeHandleVariants({
58
+ direction: 'right',
59
+ })}
60
+ options={{ direction: 'right' }}
61
+ />
62
+ </Resizable>
63
+
64
+ <Caption style={{ width }} align={align}>
65
+ <CaptionTextarea
66
+ readOnly={readOnly}
67
+ onFocus={(e) => {
68
+ e.preventDefault();
69
+ }}
70
+ placeholder="Write a caption..."
71
+ />
72
+ </Caption>
73
+ </figure>
74
+
75
+ {props.children}
76
+ </PlateElement>
77
+ </MediaToolbar>
78
+ );
79
+ }
80
+ );
@@ -0,0 +1,249 @@
1
+ import React from 'react';
2
+
3
+ import type { TPlaceholderElement } from 'platejs';
4
+ import type { PlateElementProps } from 'platejs/react';
5
+
6
+ import {
7
+ PlaceholderPlugin,
8
+ PlaceholderProvider,
9
+ updateUploadHistory,
10
+ } from '@platejs/media/react';
11
+ import { AudioLines, FileUp, Film, ImageIcon, Loader2Icon } from 'lucide-react';
12
+ import { KEYS } from 'platejs';
13
+ import { PlateElement, useEditorPlugin, withHOC } from 'platejs/react';
14
+ import { useFilePicker } from 'use-file-picker';
15
+
16
+ import { cn } from '@/lib/utils';
17
+ import { useUploadFile } from '@/hooks/use-upload-file';
18
+
19
+ const CONTENT: Record<
20
+ string,
21
+ {
22
+ accept: string[];
23
+ content: React.ReactNode;
24
+ icon: React.ReactNode;
25
+ }
26
+ > = {
27
+ [KEYS.audio]: {
28
+ accept: ['audio/*'],
29
+ content: 'Add an audio file',
30
+ icon: <AudioLines />,
31
+ },
32
+ [KEYS.file]: {
33
+ accept: ['*'],
34
+ content: 'Add a file',
35
+ icon: <FileUp />,
36
+ },
37
+ [KEYS.img]: {
38
+ accept: ['image/*'],
39
+ content: 'Add an image',
40
+ icon: <ImageIcon />,
41
+ },
42
+ [KEYS.video]: {
43
+ accept: ['video/*'],
44
+ content: 'Add a video',
45
+ icon: <Film />,
46
+ },
47
+ };
48
+
49
+ export const PlaceholderElement = withHOC(
50
+ PlaceholderProvider,
51
+ function PlaceholderElement(props: PlateElementProps<TPlaceholderElement>) {
52
+ const { editor, element } = props;
53
+
54
+ const { api } = useEditorPlugin(PlaceholderPlugin);
55
+
56
+ const { isUploading, progress, uploadedFile, uploadFile, uploadingFile } =
57
+ useUploadFile();
58
+
59
+ const loading = isUploading && uploadingFile;
60
+
61
+ const currentContent = CONTENT[element.mediaType];
62
+
63
+ const isImage = element.mediaType === KEYS.img;
64
+
65
+ const imageRef = React.useRef<HTMLImageElement>(null);
66
+
67
+ const { openFilePicker } = useFilePicker({
68
+ accept: currentContent.accept,
69
+ multiple: true,
70
+ onFilesSelected: ({ plainFiles: updatedFiles }) => {
71
+ const firstFile = updatedFiles[0];
72
+ const restFiles = updatedFiles.slice(1);
73
+
74
+ replaceCurrentPlaceholder(firstFile);
75
+
76
+ if (restFiles.length > 0) {
77
+ editor.getTransforms(PlaceholderPlugin).insert.media(restFiles);
78
+ }
79
+ },
80
+ });
81
+
82
+ const replaceCurrentPlaceholder = React.useCallback(
83
+ (file: File) => {
84
+ void uploadFile(file);
85
+ api.placeholder.addUploadingFile(element.id as string, file);
86
+ },
87
+ [api.placeholder, element.id, uploadFile]
88
+ );
89
+
90
+ React.useEffect(() => {
91
+ if (!uploadedFile) return;
92
+
93
+ const path = editor.api.findPath(element);
94
+
95
+ editor.tf.withoutSaving(() => {
96
+ editor.tf.removeNodes({ at: path });
97
+
98
+ const node = {
99
+ children: [{ text: '' }],
100
+ initialHeight: imageRef.current?.height,
101
+ initialWidth: imageRef.current?.width,
102
+ isUpload: true,
103
+ name: element.mediaType === KEYS.file ? uploadedFile.name : '',
104
+ placeholderId: element.id as string,
105
+ type: element.mediaType!,
106
+ url: uploadedFile.url,
107
+ };
108
+
109
+ editor.tf.insertNodes(node, { at: path });
110
+
111
+ updateUploadHistory(editor, node);
112
+ });
113
+
114
+ api.placeholder.removeUploadingFile(element.id as string);
115
+ // eslint-disable-next-line react-hooks/exhaustive-deps
116
+ }, [uploadedFile, element.id]);
117
+
118
+ // React dev mode will call React.useEffect twice
119
+ const isReplaced = React.useRef(false);
120
+
121
+ /** Paste and drop */
122
+ React.useEffect(() => {
123
+ if (isReplaced.current) return;
124
+
125
+ isReplaced.current = true;
126
+ const currentFiles = api.placeholder.getUploadingFile(
127
+ element.id as string
128
+ );
129
+
130
+ if (!currentFiles) return;
131
+
132
+ replaceCurrentPlaceholder(currentFiles);
133
+
134
+ // eslint-disable-next-line react-hooks/exhaustive-deps
135
+ }, [isReplaced]);
136
+
137
+ return (
138
+ <PlateElement className="my-1" {...props}>
139
+ {(!loading || !isImage) && (
140
+ <div
141
+ className={cn(
142
+ 'flex cursor-pointer select-none items-center rounded-sm bg-muted p-3 pr-9 hover:bg-primary/10'
143
+ )}
144
+ onClick={() => !loading && openFilePicker()}
145
+ contentEditable={false}
146
+ >
147
+ <div className="relative mr-3 flex text-muted-foreground/80 [&_svg]:size-6">
148
+ {currentContent.icon}
149
+ </div>
150
+ <div className="whitespace-nowrap text-muted-foreground text-sm">
151
+ <div>
152
+ {loading ? uploadingFile?.name : currentContent.content}
153
+ </div>
154
+
155
+ {loading && !isImage && (
156
+ <div className="mt-1 flex items-center gap-1.5">
157
+ <div>{formatBytes(uploadingFile?.size ?? 0)}</div>
158
+ <div>–</div>
159
+ <div className="flex items-center">
160
+ <Loader2Icon className="mr-1 size-3.5 animate-spin text-muted-foreground" />
161
+ {progress ?? 0}%
162
+ </div>
163
+ </div>
164
+ )}
165
+ </div>
166
+ </div>
167
+ )}
168
+
169
+ {isImage && loading && (
170
+ <ImageProgress
171
+ file={uploadingFile}
172
+ imageRef={imageRef}
173
+ progress={progress}
174
+ />
175
+ )}
176
+
177
+ {props.children}
178
+ </PlateElement>
179
+ );
180
+ }
181
+ );
182
+
183
+ export function ImageProgress({
184
+ className,
185
+ file,
186
+ imageRef,
187
+ progress = 0,
188
+ }: {
189
+ file: File;
190
+ className?: string;
191
+ imageRef?: React.RefObject<HTMLImageElement | null>;
192
+ progress?: number;
193
+ }) {
194
+ const [objectUrl, setObjectUrl] = React.useState<string | null>(null);
195
+
196
+ React.useEffect(() => {
197
+ const url = URL.createObjectURL(file);
198
+ setObjectUrl(url);
199
+
200
+ return () => {
201
+ URL.revokeObjectURL(url);
202
+ };
203
+ }, [file]);
204
+
205
+ if (!objectUrl) {
206
+ return null;
207
+ }
208
+
209
+ return (
210
+ <div className={cn('relative', className)} contentEditable={false}>
211
+ <img
212
+ ref={imageRef}
213
+ className="h-auto w-full rounded-sm object-cover"
214
+ alt={file.name}
215
+ src={objectUrl}
216
+ />
217
+ {progress < 100 && (
218
+ <div className="absolute right-1 bottom-1 flex items-center space-x-2 rounded-full bg-black/50 px-1 py-0.5">
219
+ <Loader2Icon className="size-3.5 animate-spin text-muted-foreground" />
220
+ <span className="font-medium text-white text-xs">
221
+ {Math.round(progress)}%
222
+ </span>
223
+ </div>
224
+ )}
225
+ </div>
226
+ );
227
+ }
228
+
229
+ function formatBytes(
230
+ bytes: number,
231
+ opts: {
232
+ decimals?: number;
233
+ sizeType?: 'accurate' | 'normal';
234
+ } = {}
235
+ ) {
236
+ const { decimals = 0, sizeType = 'normal' } = opts;
237
+
238
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
239
+ const accurateSizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB'];
240
+
241
+ if (bytes === 0) return '0 Byte';
242
+
243
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
244
+
245
+ return `${(bytes / 1024 ** i).toFixed(decimals)} ${sizeType === 'accurate'
246
+ ? (accurateSizes[i] ?? 'Bytest')
247
+ : (sizes[i] ?? 'Bytes')
248
+ }`;
249
+ }