@questpie/admin 0.0.1 → 1.0.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 (250) hide show
  1. package/README.md +439 -424
  2. package/dist/auth-layout-M8K8_q5R.mjs +181 -0
  3. package/dist/auth-layout-M8K8_q5R.mjs.map +1 -0
  4. package/dist/bulk-upload-dialog-h7zXD78Y.mjs +274 -0
  5. package/dist/bulk-upload-dialog-h7zXD78Y.mjs.map +1 -0
  6. package/dist/{components/ui/card.mjs → card-BKHjBQfw.mjs} +8 -8
  7. package/dist/card-BKHjBQfw.mjs.map +1 -0
  8. package/dist/client/styles/index.css +434 -0
  9. package/dist/client-BCGpkAz6.mjs +22635 -0
  10. package/dist/client-BCGpkAz6.mjs.map +1 -0
  11. package/dist/client-CcWZbkBP.d.mts +13585 -0
  12. package/dist/client-CcWZbkBP.d.mts.map +1 -0
  13. package/dist/client.d.mts +3 -0
  14. package/dist/client.mjs +14 -0
  15. package/dist/content-locales-provider-BXvuIgfg.mjs +1650 -0
  16. package/dist/content-locales-provider-BXvuIgfg.mjs.map +1 -0
  17. package/dist/dashboard-page-B4PGEdc2.mjs +2500 -0
  18. package/dist/dashboard-page-B4PGEdc2.mjs.map +1 -0
  19. package/dist/dashboard-page-CVlyR40m.mjs +6 -0
  20. package/dist/dropzone-Do3awXKd.mjs +634 -0
  21. package/dist/dropzone-Do3awXKd.mjs.map +1 -0
  22. package/dist/{views/auth/forgot-password-form.mjs → forgot-password-page-Bcp-An4Y.mjs} +87 -14
  23. package/dist/forgot-password-page-Bcp-An4Y.mjs.map +1 -0
  24. package/dist/forgot-password-page-CIILVhfo.mjs +7 -0
  25. package/dist/index-B9Xwk4hi.d.mts +2753 -0
  26. package/dist/index-B9Xwk4hi.d.mts.map +1 -0
  27. package/dist/index.d.mts +3 -0
  28. package/dist/index.mjs +14 -0
  29. package/dist/login-page-8K7fo0qK.mjs +7 -0
  30. package/dist/login-page-CP4gA-dl.mjs +298 -0
  31. package/dist/login-page-CP4gA-dl.mjs.map +1 -0
  32. package/dist/preview-utils-BKQ9-TMa.mjs +65 -0
  33. package/dist/preview-utils-BKQ9-TMa.mjs.map +1 -0
  34. package/dist/{views/auth/reset-password-form.mjs → reset-password-page-BqfDmLxA.mjs} +111 -14
  35. package/dist/reset-password-page-BqfDmLxA.mjs.map +1 -0
  36. package/dist/reset-password-page-DLATv0xQ.mjs +7 -0
  37. package/dist/runtime-6VZM878K.mjs +69 -0
  38. package/dist/runtime-6VZM878K.mjs.map +1 -0
  39. package/dist/saved-views.types-BMsz5mCy.d.mts +42 -0
  40. package/dist/saved-views.types-BMsz5mCy.d.mts.map +1 -0
  41. package/dist/server.d.mts +250 -0
  42. package/dist/server.d.mts.map +1 -0
  43. package/dist/server.mjs +832 -0
  44. package/dist/server.mjs.map +1 -0
  45. package/dist/setup-page-CMZ5P_OE.mjs +6 -0
  46. package/dist/setup-page-YAP_fzqh.mjs +264 -0
  47. package/dist/setup-page-YAP_fzqh.mjs.map +1 -0
  48. package/dist/shared.d.mts +57 -0
  49. package/dist/shared.d.mts.map +1 -0
  50. package/dist/shared.mjs +3 -0
  51. package/dist/{hooks/use-auth.mjs → use-auth-BoLmWtmU.mjs} +42 -30
  52. package/dist/use-auth-BoLmWtmU.mjs.map +1 -0
  53. package/package.json +48 -197
  54. package/.turbo/turbo-build.log +0 -108
  55. package/CHANGELOG.md +0 -10
  56. package/STATUS.md +0 -917
  57. package/VALIDATION.md +0 -602
  58. package/components.json +0 -24
  59. package/dist/__tests__/setup.mjs +0 -38
  60. package/dist/__tests__/test-utils.mjs +0 -45
  61. package/dist/__tests__/vitest.d.mjs +0 -3
  62. package/dist/components/admin-app.mjs +0 -69
  63. package/dist/components/fields/array-field.mjs +0 -190
  64. package/dist/components/fields/checkbox-field.mjs +0 -34
  65. package/dist/components/fields/custom-field.mjs +0 -32
  66. package/dist/components/fields/date-field.mjs +0 -41
  67. package/dist/components/fields/datetime-field.mjs +0 -42
  68. package/dist/components/fields/email-field.mjs +0 -37
  69. package/dist/components/fields/embedded-collection.mjs +0 -253
  70. package/dist/components/fields/field-types.mjs +0 -1
  71. package/dist/components/fields/field-utils.mjs +0 -10
  72. package/dist/components/fields/field-wrapper.mjs +0 -34
  73. package/dist/components/fields/index.mjs +0 -23
  74. package/dist/components/fields/json-field.mjs +0 -243
  75. package/dist/components/fields/locale-badge.mjs +0 -16
  76. package/dist/components/fields/number-field.mjs +0 -39
  77. package/dist/components/fields/password-field.mjs +0 -37
  78. package/dist/components/fields/relation-field.mjs +0 -104
  79. package/dist/components/fields/relation-picker.mjs +0 -229
  80. package/dist/components/fields/relation-select.mjs +0 -188
  81. package/dist/components/fields/rich-text-editor/index.mjs +0 -897
  82. package/dist/components/fields/select-field.mjs +0 -41
  83. package/dist/components/fields/switch-field.mjs +0 -34
  84. package/dist/components/fields/text-field.mjs +0 -38
  85. package/dist/components/fields/textarea-field.mjs +0 -38
  86. package/dist/components/index.mjs +0 -59
  87. package/dist/components/primitives/checkbox-input.mjs +0 -127
  88. package/dist/components/primitives/date-input.mjs +0 -303
  89. package/dist/components/primitives/index.mjs +0 -12
  90. package/dist/components/primitives/number-input.mjs +0 -104
  91. package/dist/components/primitives/select-input.mjs +0 -177
  92. package/dist/components/primitives/tag-input.mjs +0 -135
  93. package/dist/components/primitives/text-input.mjs +0 -39
  94. package/dist/components/primitives/textarea-input.mjs +0 -37
  95. package/dist/components/primitives/toggle-input.mjs +0 -31
  96. package/dist/components/primitives/types.mjs +0 -12
  97. package/dist/components/ui/accordion.mjs +0 -55
  98. package/dist/components/ui/avatar.mjs +0 -54
  99. package/dist/components/ui/badge.mjs +0 -34
  100. package/dist/components/ui/button.mjs +0 -48
  101. package/dist/components/ui/checkbox.mjs +0 -21
  102. package/dist/components/ui/combobox.mjs +0 -163
  103. package/dist/components/ui/dialog.mjs +0 -95
  104. package/dist/components/ui/dropdown-menu.mjs +0 -138
  105. package/dist/components/ui/field.mjs +0 -113
  106. package/dist/components/ui/input-group.mjs +0 -82
  107. package/dist/components/ui/input.mjs +0 -17
  108. package/dist/components/ui/label.mjs +0 -15
  109. package/dist/components/ui/popover.mjs +0 -56
  110. package/dist/components/ui/scroll-area.mjs +0 -38
  111. package/dist/components/ui/select.mjs +0 -100
  112. package/dist/components/ui/separator.mjs +0 -16
  113. package/dist/components/ui/sheet.mjs +0 -90
  114. package/dist/components/ui/sidebar.mjs +0 -387
  115. package/dist/components/ui/skeleton.mjs +0 -14
  116. package/dist/components/ui/spinner.mjs +0 -16
  117. package/dist/components/ui/switch.mjs +0 -22
  118. package/dist/components/ui/table.mjs +0 -68
  119. package/dist/components/ui/tabs.mjs +0 -48
  120. package/dist/components/ui/textarea.mjs +0 -15
  121. package/dist/components/ui/tooltip.mjs +0 -44
  122. package/dist/config/component-registry.mjs +0 -38
  123. package/dist/config/index.mjs +0 -129
  124. package/dist/hooks/admin-provider.mjs +0 -70
  125. package/dist/hooks/index.mjs +0 -7
  126. package/dist/hooks/store.mjs +0 -178
  127. package/dist/hooks/use-collection-db.mjs +0 -146
  128. package/dist/hooks/use-collection.mjs +0 -112
  129. package/dist/hooks/use-global.mjs +0 -46
  130. package/dist/hooks/use-mobile.mjs +0 -20
  131. package/dist/lib/utils.mjs +0 -10
  132. package/dist/styles/index.css +0 -336
  133. package/dist/styles/index.mjs +0 -1
  134. package/dist/utils/index.mjs +0 -9
  135. package/dist/views/auth/auth-layout.mjs +0 -52
  136. package/dist/views/auth/index.mjs +0 -6
  137. package/dist/views/auth/login-form.mjs +0 -156
  138. package/dist/views/collection/auto-form-fields.mjs +0 -525
  139. package/dist/views/collection/collection-form.mjs +0 -91
  140. package/dist/views/collection/collection-list.mjs +0 -76
  141. package/dist/views/collection/form-field.mjs +0 -42
  142. package/dist/views/collection/index.mjs +0 -6
  143. package/dist/views/common/index.mjs +0 -4
  144. package/dist/views/common/locale-switcher.mjs +0 -39
  145. package/dist/views/common/version-history.mjs +0 -272
  146. package/dist/views/index.mjs +0 -9
  147. package/dist/views/layout/admin-layout.mjs +0 -40
  148. package/dist/views/layout/admin-router.mjs +0 -95
  149. package/dist/views/layout/admin-sidebar.mjs +0 -63
  150. package/dist/views/layout/index.mjs +0 -5
  151. package/src/__tests__/setup.ts +0 -44
  152. package/src/__tests__/test-utils.tsx +0 -49
  153. package/src/__tests__/vitest.d.ts +0 -9
  154. package/src/components/admin-app.tsx +0 -221
  155. package/src/components/fields/array-field.tsx +0 -237
  156. package/src/components/fields/checkbox-field.tsx +0 -47
  157. package/src/components/fields/custom-field.tsx +0 -50
  158. package/src/components/fields/date-field.tsx +0 -65
  159. package/src/components/fields/datetime-field.tsx +0 -67
  160. package/src/components/fields/email-field.tsx +0 -51
  161. package/src/components/fields/embedded-collection.tsx +0 -315
  162. package/src/components/fields/field-types.ts +0 -162
  163. package/src/components/fields/field-utils.ts +0 -6
  164. package/src/components/fields/field-wrapper.tsx +0 -52
  165. package/src/components/fields/index.ts +0 -66
  166. package/src/components/fields/json-field.tsx +0 -440
  167. package/src/components/fields/locale-badge.tsx +0 -15
  168. package/src/components/fields/number-field.tsx +0 -57
  169. package/src/components/fields/password-field.tsx +0 -51
  170. package/src/components/fields/relation-field.tsx +0 -243
  171. package/src/components/fields/relation-picker.tsx +0 -402
  172. package/src/components/fields/relation-select.tsx +0 -327
  173. package/src/components/fields/rich-text-editor/index.tsx +0 -1337
  174. package/src/components/fields/select-field.tsx +0 -61
  175. package/src/components/fields/switch-field.tsx +0 -47
  176. package/src/components/fields/text-field.tsx +0 -55
  177. package/src/components/fields/textarea-field.tsx +0 -55
  178. package/src/components/index.ts +0 -40
  179. package/src/components/primitives/checkbox-input.tsx +0 -193
  180. package/src/components/primitives/date-input.tsx +0 -401
  181. package/src/components/primitives/index.ts +0 -24
  182. package/src/components/primitives/number-input.tsx +0 -132
  183. package/src/components/primitives/select-input.tsx +0 -296
  184. package/src/components/primitives/tag-input.tsx +0 -200
  185. package/src/components/primitives/text-input.tsx +0 -49
  186. package/src/components/primitives/textarea-input.tsx +0 -46
  187. package/src/components/primitives/toggle-input.tsx +0 -36
  188. package/src/components/primitives/types.ts +0 -235
  189. package/src/components/ui/accordion.tsx +0 -72
  190. package/src/components/ui/avatar.tsx +0 -106
  191. package/src/components/ui/badge.tsx +0 -48
  192. package/src/components/ui/button.tsx +0 -53
  193. package/src/components/ui/card.tsx +0 -94
  194. package/src/components/ui/checkbox.tsx +0 -27
  195. package/src/components/ui/combobox.tsx +0 -290
  196. package/src/components/ui/dialog.tsx +0 -151
  197. package/src/components/ui/dropdown-menu.tsx +0 -254
  198. package/src/components/ui/field.tsx +0 -227
  199. package/src/components/ui/input-group.tsx +0 -149
  200. package/src/components/ui/input.tsx +0 -20
  201. package/src/components/ui/label.tsx +0 -18
  202. package/src/components/ui/popover.tsx +0 -88
  203. package/src/components/ui/scroll-area.tsx +0 -53
  204. package/src/components/ui/select.tsx +0 -192
  205. package/src/components/ui/separator.tsx +0 -23
  206. package/src/components/ui/sheet.tsx +0 -127
  207. package/src/components/ui/sidebar.tsx +0 -723
  208. package/src/components/ui/skeleton.tsx +0 -13
  209. package/src/components/ui/spinner.tsx +0 -10
  210. package/src/components/ui/switch.tsx +0 -32
  211. package/src/components/ui/table.tsx +0 -99
  212. package/src/components/ui/tabs.tsx +0 -82
  213. package/src/components/ui/textarea.tsx +0 -18
  214. package/src/components/ui/tooltip.tsx +0 -70
  215. package/src/config/component-registry.ts +0 -190
  216. package/src/config/index.ts +0 -1099
  217. package/src/hooks/README.md +0 -269
  218. package/src/hooks/admin-provider.tsx +0 -110
  219. package/src/hooks/index.ts +0 -41
  220. package/src/hooks/store.ts +0 -248
  221. package/src/hooks/use-auth.ts +0 -168
  222. package/src/hooks/use-collection-db.ts +0 -209
  223. package/src/hooks/use-collection.ts +0 -156
  224. package/src/hooks/use-global.ts +0 -69
  225. package/src/hooks/use-mobile.ts +0 -21
  226. package/src/lib/utils.ts +0 -6
  227. package/src/styles/index.css +0 -340
  228. package/src/utils/index.ts +0 -6
  229. package/src/views/auth/auth-layout.tsx +0 -77
  230. package/src/views/auth/forgot-password-form.tsx +0 -192
  231. package/src/views/auth/index.ts +0 -21
  232. package/src/views/auth/login-form.tsx +0 -229
  233. package/src/views/auth/reset-password-form.tsx +0 -232
  234. package/src/views/collection/auto-form-fields.tsx +0 -982
  235. package/src/views/collection/collection-form.tsx +0 -186
  236. package/src/views/collection/collection-list.tsx +0 -223
  237. package/src/views/collection/form-field.tsx +0 -52
  238. package/src/views/collection/index.ts +0 -15
  239. package/src/views/common/index.ts +0 -8
  240. package/src/views/common/locale-switcher.tsx +0 -45
  241. package/src/views/common/version-history.tsx +0 -406
  242. package/src/views/index.ts +0 -25
  243. package/src/views/layout/admin-layout.tsx +0 -117
  244. package/src/views/layout/admin-router.tsx +0 -206
  245. package/src/views/layout/admin-sidebar.tsx +0 -185
  246. package/src/views/layout/index.ts +0 -12
  247. package/tsconfig.json +0 -13
  248. package/tsconfig.tsbuildinfo +0 -1
  249. package/tsdown.config.ts +0 -13
  250. package/vitest.config.ts +0 -29
@@ -1,1337 +0,0 @@
1
- /**
2
- * RichTextEditor Component
3
- *
4
- * Tiptap-based rich text editor with toolbar controls.
5
- */
6
-
7
- import * as React from "react";
8
- import { Extension, type Editor, type Range } from "@tiptap/core";
9
- import type { Extension as TiptapExtension } from "@tiptap/core";
10
- import {
11
- BubbleMenu,
12
- EditorContent,
13
- ReactRenderer,
14
- useEditor,
15
- } from "@tiptap/react";
16
- import StarterKit from "@tiptap/starter-kit";
17
- import Underline from "@tiptap/extension-underline";
18
- import Link from "@tiptap/extension-link";
19
- import Image from "@tiptap/extension-image";
20
- import Placeholder from "@tiptap/extension-placeholder";
21
- import TextAlign from "@tiptap/extension-text-align";
22
- import Table from "@tiptap/extension-table";
23
- import TableRow from "@tiptap/extension-table-row";
24
- import TableHeader from "@tiptap/extension-table-header";
25
- import TableCell from "@tiptap/extension-table-cell";
26
- import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
27
- import CharacterCount from "@tiptap/extension-character-count";
28
- import Suggestion from "@tiptap/suggestion";
29
- import tippy, { type Instance } from "tippy.js";
30
- import { common, createLowlight } from "lowlight";
31
-
32
- const lowlight = createLowlight(common);
33
- import { Button } from "../../ui/button";
34
- import { Input } from "../../ui/input";
35
- import { Label } from "../../ui/label";
36
- import { LocaleBadge } from "../locale-badge";
37
- import {
38
- Popover,
39
- PopoverContent,
40
- PopoverHeader,
41
- PopoverTitle,
42
- PopoverTrigger,
43
- } from "../../ui/popover";
44
- import type { FieldComponentProps } from "../../../config/component-registry";
45
- import { cn } from "../../../utils";
46
-
47
- export type RichTextFeatures = {
48
- toolbar?: boolean;
49
- bubbleMenu?: boolean;
50
- slashCommands?: boolean;
51
- history?: boolean;
52
- heading?: boolean;
53
- bold?: boolean;
54
- italic?: boolean;
55
- underline?: boolean;
56
- strike?: boolean;
57
- code?: boolean;
58
- codeBlock?: boolean;
59
- blockquote?: boolean;
60
- bulletList?: boolean;
61
- orderedList?: boolean;
62
- horizontalRule?: boolean;
63
- align?: boolean;
64
- link?: boolean;
65
- image?: boolean;
66
- table?: boolean;
67
- tableControls?: boolean;
68
- characterCount?: boolean;
69
- };
70
-
71
- export interface RichTextEditorProps extends FieldComponentProps<any> {
72
- /**
73
- * Output format
74
- */
75
- outputFormat?: "json" | "html" | "markdown";
76
-
77
- /**
78
- * Custom Tiptap extensions
79
- */
80
- extensions?: TiptapExtension[];
81
-
82
- /**
83
- * Feature toggles
84
- */
85
- features?: RichTextFeatures;
86
-
87
- /**
88
- * Show character count
89
- */
90
- showCharacterCount?: boolean;
91
-
92
- /**
93
- * Max character limit
94
- */
95
- maxCharacters?: number;
96
-
97
- /**
98
- * Enable image uploads
99
- */
100
- enableImages?: boolean;
101
-
102
- /**
103
- * Image upload handler
104
- */
105
- onImageUpload?: (file: File) => Promise<string>;
106
- }
107
-
108
- type OutputValue = Record<string, any> | string;
109
-
110
- type ToolbarButtonProps = {
111
- active?: boolean;
112
- disabled?: boolean;
113
- title?: string;
114
- onClick: () => void;
115
- children: React.ReactNode;
116
- };
117
-
118
- type SlashCommandItem = {
119
- title: string;
120
- description?: string;
121
- keywords?: string[];
122
- command: (editor: any) => void;
123
- };
124
-
125
- type SlashCommandListProps = {
126
- items: SlashCommandItem[];
127
- command: (item: SlashCommandItem) => void;
128
- };
129
-
130
- type SlashCommandListHandle = {
131
- onKeyDown: (props: { event: KeyboardEvent }) => boolean;
132
- };
133
-
134
- const defaultFeatures: Required<RichTextFeatures> = {
135
- toolbar: true,
136
- bubbleMenu: true,
137
- slashCommands: true,
138
- history: true,
139
- heading: true,
140
- bold: true,
141
- italic: true,
142
- underline: true,
143
- strike: true,
144
- code: true,
145
- codeBlock: true,
146
- blockquote: true,
147
- bulletList: true,
148
- orderedList: true,
149
- horizontalRule: true,
150
- align: true,
151
- link: true,
152
- image: true,
153
- table: true,
154
- tableControls: true,
155
- characterCount: true,
156
- };
157
-
158
- function ToolbarButton({
159
- active,
160
- disabled,
161
- title,
162
- onClick,
163
- children,
164
- }: ToolbarButtonProps) {
165
- return (
166
- <Button
167
- type="button"
168
- variant="ghost"
169
- size="xs"
170
- data-active={active}
171
- title={title}
172
- disabled={disabled}
173
- onClick={onClick}
174
- className="data-[active=true]:bg-muted data-[active=true]:text-foreground"
175
- >
176
- {children}
177
- </Button>
178
- );
179
- }
180
-
181
- function ToolbarGroup({ children }: { children: React.ReactNode }) {
182
- return (
183
- <div className="flex items-center gap-1 border-r border-border pr-2 last:border-r-0 last:pr-0">
184
- {children}
185
- </div>
186
- );
187
- }
188
-
189
- const SlashCommandList = React.forwardRef<
190
- SlashCommandListHandle,
191
- SlashCommandListProps
192
- >(function SlashCommandList({ items, command }, ref) {
193
- const [selectedIndex, setSelectedIndex] = React.useState(0);
194
-
195
- React.useEffect(() => {
196
- setSelectedIndex(0);
197
- }, [items]);
198
-
199
- const selectItem = React.useCallback(
200
- (index: number) => {
201
- const item = items[index];
202
- if (item) {
203
- command(item);
204
- }
205
- },
206
- [command, items],
207
- );
208
-
209
- React.useImperativeHandle(ref, () => ({
210
- onKeyDown: ({ event }) => {
211
- if (items.length === 0) return false;
212
- if (event.key === "ArrowDown") {
213
- setSelectedIndex((prev) => (prev + 1) % items.length);
214
- return true;
215
- }
216
-
217
- if (event.key === "ArrowUp") {
218
- setSelectedIndex((prev) => (prev - 1 + items.length) % items.length);
219
- return true;
220
- }
221
-
222
- if (event.key === "Enter") {
223
- selectItem(selectedIndex);
224
- return true;
225
- }
226
-
227
- return false;
228
- },
229
- }));
230
-
231
- return (
232
- <div className="qp-rich-text-editor__slash">
233
- {items.length === 0 && (
234
- <div className="qp-rich-text-editor__slash-empty">No results</div>
235
- )}
236
- {items.map((item, index) => (
237
- <button
238
- key={item.title}
239
- type="button"
240
- className={cn(
241
- "qp-rich-text-editor__slash-item",
242
- index === selectedIndex
243
- ? "qp-rich-text-editor__slash-item--active"
244
- : "",
245
- )}
246
- onClick={() => selectItem(index)}
247
- >
248
- <span className="qp-rich-text-editor__slash-title">{item.title}</span>
249
- {item.description && (
250
- <span className="qp-rich-text-editor__slash-description">
251
- {item.description}
252
- </span>
253
- )}
254
- </button>
255
- ))}
256
- </div>
257
- );
258
- });
259
-
260
- function getHeadingLevel(editor: ReturnType<typeof useEditor>) {
261
- if (!editor) return "paragraph";
262
- for (let level = 1; level <= 6; level += 1) {
263
- if (editor.isActive("heading", { level })) {
264
- return String(level);
265
- }
266
- }
267
- return "paragraph";
268
- }
269
-
270
- function getOutput(
271
- editor: NonNullable<ReturnType<typeof useEditor>>,
272
- outputFormat: "json" | "html" | "markdown",
273
- ) {
274
- if (outputFormat === "html") {
275
- return editor.getHTML();
276
- }
277
-
278
- if (outputFormat === "markdown") {
279
- const markdown = (editor.storage as any)?.markdown?.getMarkdown?.();
280
- if (typeof markdown === "string") {
281
- return markdown;
282
- }
283
- return editor.getHTML();
284
- }
285
-
286
- return editor.getJSON();
287
- }
288
-
289
- function isSameValue(a: OutputValue | undefined, b: OutputValue | undefined) {
290
- if (a === b) return true;
291
- if (!a || !b) return false;
292
- if (typeof a === "string" && typeof b === "string") return a === b;
293
- try {
294
- return JSON.stringify(a) === JSON.stringify(b);
295
- } catch {
296
- return false;
297
- }
298
- }
299
-
300
- function createSlashCommandExtension(
301
- getItems: (editor: Editor) => SlashCommandItem[],
302
- ) {
303
- return Extension.create({
304
- name: "slashCommand",
305
- addOptions() {
306
- return {
307
- suggestion: {
308
- char: "/",
309
- startOfLine: true,
310
- command: ({
311
- editor,
312
- range,
313
- props,
314
- }: {
315
- editor: Editor;
316
- range: Range;
317
- props: SlashCommandItem;
318
- }) => {
319
- editor.chain().focus().deleteRange(range).run();
320
- props.command(editor);
321
- },
322
- items: ({ query, editor }: { query: string; editor: Editor }) => {
323
- const items = getItems(editor);
324
- if (!query) return items;
325
- const search = query.toLowerCase();
326
- return items.filter((item) => {
327
- return (
328
- item.title.toLowerCase().includes(search) ||
329
- item.description?.toLowerCase().includes(search) ||
330
- item.keywords?.some((keyword) =>
331
- keyword.toLowerCase().includes(search),
332
- )
333
- );
334
- });
335
- },
336
- render: () => {
337
- let component: ReactRenderer<SlashCommandListHandle> | null = null;
338
- let popup: Instance[] | null = null;
339
-
340
- return {
341
- onStart: (props: any) => {
342
- component = new ReactRenderer(SlashCommandList, {
343
- props,
344
- editor: props.editor,
345
- });
346
-
347
- if (!props.clientRect) {
348
- return;
349
- }
350
-
351
- popup = tippy("body", {
352
- getReferenceClientRect: props.clientRect,
353
- appendTo: () => document.body,
354
- content: component.element,
355
- showOnCreate: true,
356
- interactive: true,
357
- trigger: "manual",
358
- placement: "bottom-start",
359
- theme: "qp-rich-text-editor",
360
- });
361
- },
362
-
363
- onUpdate: (props: any) => {
364
- component?.updateProps(props);
365
-
366
- if (!props.clientRect) {
367
- return;
368
- }
369
-
370
- popup?.[0].setProps({
371
- getReferenceClientRect: props.clientRect,
372
- });
373
- },
374
-
375
- onKeyDown: (props: any) => {
376
- if (props.event.key === "Escape") {
377
- popup?.[0].hide();
378
- return true;
379
- }
380
-
381
- return component?.ref?.onKeyDown(props) ?? false;
382
- },
383
-
384
- onExit: () => {
385
- popup?.[0].destroy();
386
- component?.destroy();
387
- },
388
- };
389
- },
390
- },
391
- };
392
- },
393
- addProseMirrorPlugins() {
394
- return [Suggestion(this.options.suggestion)];
395
- },
396
- });
397
- }
398
-
399
- export function RichTextEditor({
400
- name,
401
- value,
402
- onChange,
403
- disabled,
404
- readOnly,
405
- label,
406
- description,
407
- placeholder,
408
- required,
409
- error,
410
- localized,
411
- locale,
412
- outputFormat = "json",
413
- extensions,
414
- features,
415
- showCharacterCount,
416
- maxCharacters,
417
- enableImages,
418
- onImageUpload,
419
- }: RichTextEditorProps) {
420
- const [linkOpen, setLinkOpen] = React.useState(false);
421
- const [linkUrl, setLinkUrl] = React.useState("");
422
- const [imageOpen, setImageOpen] = React.useState(false);
423
- const [imageUrl, setImageUrl] = React.useState("");
424
- const [imageAlt, setImageAlt] = React.useState("");
425
- const [uploadingImage, setUploadingImage] = React.useState(false);
426
- const fileInputRef = React.useRef<HTMLInputElement | null>(null);
427
-
428
- const resolvedFeatures = React.useMemo(
429
- () => ({
430
- ...defaultFeatures,
431
- ...features,
432
- }),
433
- [features],
434
- );
435
-
436
- const allowImages = resolvedFeatures.image && (enableImages ?? true);
437
- const allowLinks = resolvedFeatures.link;
438
- const allowTables = resolvedFeatures.table;
439
- const allowTableControls = resolvedFeatures.tableControls && allowTables;
440
- const allowSlashCommands = resolvedFeatures.slashCommands;
441
- const allowBubbleMenu = resolvedFeatures.bubbleMenu;
442
- const allowToolbar = resolvedFeatures.toolbar;
443
- const allowCharacterCount =
444
- resolvedFeatures.characterCount && (showCharacterCount || maxCharacters);
445
-
446
- const resolvedExtensions = React.useMemo(() => {
447
- const starterKitConfig: Record<string, any> = {
448
- codeBlock: false,
449
- };
450
-
451
- if (!resolvedFeatures.bold) starterKitConfig.bold = false;
452
- if (!resolvedFeatures.italic) starterKitConfig.italic = false;
453
- if (!resolvedFeatures.strike) starterKitConfig.strike = false;
454
- if (!resolvedFeatures.code) starterKitConfig.code = false;
455
- if (!resolvedFeatures.blockquote) starterKitConfig.blockquote = false;
456
- if (!resolvedFeatures.heading) starterKitConfig.heading = false;
457
- if (!resolvedFeatures.bulletList) starterKitConfig.bulletList = false;
458
- if (!resolvedFeatures.orderedList) starterKitConfig.orderedList = false;
459
- if (!resolvedFeatures.bulletList && !resolvedFeatures.orderedList) {
460
- starterKitConfig.listItem = false;
461
- }
462
- if (!resolvedFeatures.horizontalRule)
463
- starterKitConfig.horizontalRule = false;
464
- if (!resolvedFeatures.history) starterKitConfig.history = false;
465
-
466
- const items: any[] = [
467
- StarterKit.configure(starterKitConfig),
468
- Placeholder.configure({
469
- placeholder: placeholder || "Start writing...",
470
- }),
471
- ];
472
-
473
- if (resolvedFeatures.underline) {
474
- items.push(Underline);
475
- }
476
-
477
- if (allowLinks) {
478
- items.push(
479
- Link.configure({
480
- openOnClick: false,
481
- autolink: true,
482
- linkOnPaste: true,
483
- }),
484
- );
485
- }
486
-
487
- if (resolvedFeatures.align) {
488
- items.push(TextAlign.configure({ types: ["heading", "paragraph"] }));
489
- }
490
-
491
- if (allowImages) {
492
- items.push(Image);
493
- }
494
-
495
- if (allowTables) {
496
- items.push(
497
- Table.configure({ resizable: true }),
498
- TableRow,
499
- TableHeader,
500
- TableCell,
501
- );
502
- }
503
-
504
- if (resolvedFeatures.codeBlock) {
505
- items.push(CodeBlockLowlight.configure({ lowlight }));
506
- }
507
-
508
- if (allowCharacterCount) {
509
- items.push(
510
- CharacterCount.configure({
511
- limit: maxCharacters,
512
- }),
513
- );
514
- }
515
-
516
- if (allowSlashCommands) {
517
- items.push(
518
- createSlashCommandExtension((editor) => {
519
- const commands: SlashCommandItem[] = [];
520
-
521
- if (resolvedFeatures.heading) {
522
- commands.push(
523
- {
524
- title: "Heading 1",
525
- description: "Large section heading",
526
- keywords: ["h1"],
527
- command: (cmdEditor) =>
528
- cmdEditor.chain().focus().toggleHeading({ level: 1 }).run(),
529
- },
530
- {
531
- title: "Heading 2",
532
- description: "Medium section heading",
533
- keywords: ["h2"],
534
- command: (cmdEditor) =>
535
- cmdEditor.chain().focus().toggleHeading({ level: 2 }).run(),
536
- },
537
- {
538
- title: "Heading 3",
539
- description: "Small section heading",
540
- keywords: ["h3"],
541
- command: (cmdEditor) =>
542
- cmdEditor.chain().focus().toggleHeading({ level: 3 }).run(),
543
- },
544
- );
545
- }
546
-
547
- commands.push({
548
- title: "Paragraph",
549
- description: "Start with plain text",
550
- keywords: ["text"],
551
- command: (cmdEditor) =>
552
- cmdEditor.chain().focus().setParagraph().run(),
553
- });
554
-
555
- if (resolvedFeatures.bulletList) {
556
- commands.push({
557
- title: "Bullet list",
558
- description: "Create a bulleted list",
559
- keywords: ["list", "ul"],
560
- command: (cmdEditor) =>
561
- cmdEditor.chain().focus().toggleBulletList().run(),
562
- });
563
- }
564
-
565
- if (resolvedFeatures.orderedList) {
566
- commands.push({
567
- title: "Numbered list",
568
- description: "Create an ordered list",
569
- keywords: ["list", "ol"],
570
- command: (cmdEditor) =>
571
- cmdEditor.chain().focus().toggleOrderedList().run(),
572
- });
573
- }
574
-
575
- if (resolvedFeatures.blockquote) {
576
- commands.push({
577
- title: "Quote",
578
- description: "Capture a quote",
579
- keywords: ["blockquote"],
580
- command: (cmdEditor) =>
581
- cmdEditor.chain().focus().toggleBlockquote().run(),
582
- });
583
- }
584
-
585
- if (resolvedFeatures.codeBlock) {
586
- commands.push({
587
- title: "Code block",
588
- description: "Insert code snippet",
589
- keywords: ["code"],
590
- command: (cmdEditor) =>
591
- cmdEditor.chain().focus().toggleCodeBlock().run(),
592
- });
593
- }
594
-
595
- if (resolvedFeatures.horizontalRule) {
596
- commands.push({
597
- title: "Divider",
598
- description: "Insert a horizontal rule",
599
- keywords: ["hr"],
600
- command: (cmdEditor) =>
601
- cmdEditor.chain().focus().setHorizontalRule().run(),
602
- });
603
- }
604
-
605
- if (allowTables) {
606
- commands.push({
607
- title: "Table",
608
- description: "Insert a 3x3 table",
609
- keywords: ["grid"],
610
- command: (cmdEditor) =>
611
- cmdEditor
612
- .chain()
613
- .focus()
614
- .insertTable({ rows: 3, cols: 3, withHeaderRow: true })
615
- .run(),
616
- });
617
- }
618
-
619
- return commands;
620
- }),
621
- );
622
- }
623
-
624
- if (extensions?.length) {
625
- items.push(...extensions);
626
- }
627
-
628
- return items;
629
- }, [
630
- allowCharacterCount,
631
- allowImages,
632
- allowLinks,
633
- allowSlashCommands,
634
- allowTables,
635
- extensions,
636
- maxCharacters,
637
- placeholder,
638
- resolvedFeatures,
639
- ]);
640
-
641
- const editor = useEditor({
642
- extensions: resolvedExtensions,
643
- content: value ?? "",
644
- editorProps: {
645
- attributes: {
646
- class: "qp-rich-text-editor__content",
647
- },
648
- },
649
- editable: !disabled && !readOnly,
650
- onUpdate: ({ editor: currentEditor }) => {
651
- if (disabled || readOnly) return;
652
- const nextValue = getOutput(currentEditor, outputFormat);
653
- onChange(nextValue as OutputValue);
654
- },
655
- });
656
-
657
- const isEditable = !disabled && !readOnly;
658
- const headingValue = getHeadingLevel(editor);
659
- const inTable = editor?.isActive("table") ?? false;
660
-
661
- React.useEffect(() => {
662
- if (!editor) return;
663
- editor.setEditable(isEditable);
664
- }, [editor, isEditable]);
665
-
666
- React.useEffect(() => {
667
- if (!editor) return;
668
- if (value === undefined) return;
669
-
670
- const currentValue = getOutput(editor, outputFormat);
671
- if (isSameValue(value as OutputValue, currentValue as OutputValue)) {
672
- return;
673
- }
674
-
675
- editor.commands.setContent(value ?? "", false);
676
- }, [editor, outputFormat, value]);
677
-
678
- React.useEffect(() => {
679
- if (!linkOpen || !editor) return;
680
- const currentLink = editor.getAttributes("link").href as string | undefined;
681
- setLinkUrl(currentLink || "");
682
- }, [editor, linkOpen]);
683
-
684
- const handleApplyLink = React.useCallback(() => {
685
- if (!editor) return;
686
- if (!linkUrl) {
687
- editor.chain().focus().unsetLink().run();
688
- setLinkOpen(false);
689
- return;
690
- }
691
-
692
- editor
693
- .chain()
694
- .focus()
695
- .setLink({
696
- href: linkUrl,
697
- target: "_blank",
698
- rel: "noopener noreferrer",
699
- })
700
- .run();
701
- setLinkOpen(false);
702
- }, [editor, linkUrl]);
703
-
704
- const handleRemoveLink = React.useCallback(() => {
705
- if (!editor) return;
706
- editor.chain().focus().unsetLink().run();
707
- setLinkOpen(false);
708
- }, [editor]);
709
-
710
- const handleInsertImageUrl = React.useCallback(() => {
711
- if (!editor || !imageUrl) return;
712
- editor
713
- .chain()
714
- .focus()
715
- .setImage({ src: imageUrl, alt: imageAlt || undefined })
716
- .run();
717
- setImageUrl("");
718
- setImageAlt("");
719
- setImageOpen(false);
720
- }, [editor, imageAlt, imageUrl]);
721
-
722
- const handleImageUpload = React.useCallback(
723
- async (event: React.ChangeEvent<HTMLInputElement>) => {
724
- const file = event.target.files?.[0];
725
- if (!file || !editor || !onImageUpload) return;
726
-
727
- try {
728
- setUploadingImage(true);
729
- const url = await onImageUpload(file);
730
- if (url) {
731
- editor
732
- .chain()
733
- .focus()
734
- .setImage({ src: url, alt: imageAlt || undefined })
735
- .run();
736
- setImageUrl("");
737
- setImageAlt("");
738
- setImageOpen(false);
739
- }
740
- } finally {
741
- setUploadingImage(false);
742
- event.target.value = "";
743
- }
744
- },
745
- [editor, imageAlt, onImageUpload],
746
- );
747
-
748
- const characterCount = React.useMemo(() => {
749
- if (!editor) return { characters: 0, words: 0 };
750
- const storage = editor.storage as any;
751
- if (storage?.characterCount) {
752
- return {
753
- characters: storage.characterCount.characters(),
754
- words: storage.characterCount.words(),
755
- };
756
- }
757
- const text = editor.getText();
758
- const words = text.trim().length ? text.trim().split(/\s+/).length : 0;
759
- return { characters: text.length, words };
760
- }, [editor, value]);
761
-
762
- return (
763
- <div className="space-y-2" data-disabled={disabled || readOnly}>
764
- {label && (
765
- <div className="flex items-center gap-2">
766
- <Label htmlFor={name}>
767
- {label}
768
- {required && <span className="text-destructive ml-1">*</span>}
769
- </Label>
770
- {localized && <LocaleBadge locale={locale || "i18n"} />}
771
- </div>
772
- )}
773
-
774
- <div
775
- className={cn(
776
- "qp-rich-text-editor rounded-md border bg-background",
777
- disabled || readOnly ? "opacity-60" : "",
778
- error ? "border-destructive" : "border-input",
779
- )}
780
- >
781
- {editor && allowToolbar && (
782
- <div className="flex flex-wrap items-center gap-2 border-b bg-muted/40 p-2">
783
- {resolvedFeatures.history && (
784
- <ToolbarGroup>
785
- <ToolbarButton
786
- disabled={!isEditable || !editor.can().undo()}
787
- title="Undo"
788
- onClick={() => editor.chain().focus().undo().run()}
789
- >
790
- Undo
791
- </ToolbarButton>
792
- <ToolbarButton
793
- disabled={!isEditable || !editor.can().redo()}
794
- title="Redo"
795
- onClick={() => editor.chain().focus().redo().run()}
796
- >
797
- Redo
798
- </ToolbarButton>
799
- </ToolbarGroup>
800
- )}
801
-
802
- {resolvedFeatures.heading && (
803
- <ToolbarGroup>
804
- <select
805
- className="h-6 rounded-sm border bg-background px-2 text-xs"
806
- value={headingValue}
807
- onChange={(event) => {
808
- if (!editor) return;
809
- const nextValue = event.target.value;
810
- if (nextValue === "paragraph") {
811
- editor.chain().focus().setParagraph().run();
812
- return;
813
- }
814
- editor
815
- .chain()
816
- .focus()
817
- .toggleHeading({
818
- level: Number(nextValue) as 1 | 2 | 3 | 4 | 5 | 6,
819
- })
820
- .run();
821
- }}
822
- disabled={!isEditable}
823
- >
824
- <option value="paragraph">Paragraph</option>
825
- <option value="1">Heading 1</option>
826
- <option value="2">Heading 2</option>
827
- <option value="3">Heading 3</option>
828
- <option value="4">Heading 4</option>
829
- <option value="5">Heading 5</option>
830
- <option value="6">Heading 6</option>
831
- </select>
832
- </ToolbarGroup>
833
- )}
834
-
835
- <ToolbarGroup>
836
- {resolvedFeatures.bold && (
837
- <ToolbarButton
838
- active={editor.isActive("bold")}
839
- disabled={!isEditable}
840
- title="Bold"
841
- onClick={() => editor.chain().focus().toggleBold().run()}
842
- >
843
- Bold
844
- </ToolbarButton>
845
- )}
846
- {resolvedFeatures.italic && (
847
- <ToolbarButton
848
- active={editor.isActive("italic")}
849
- disabled={!isEditable}
850
- title="Italic"
851
- onClick={() => editor.chain().focus().toggleItalic().run()}
852
- >
853
- Italic
854
- </ToolbarButton>
855
- )}
856
- {resolvedFeatures.underline && (
857
- <ToolbarButton
858
- active={editor.isActive("underline")}
859
- disabled={!isEditable}
860
- title="Underline"
861
- onClick={() => editor.chain().focus().toggleUnderline().run()}
862
- >
863
- Underline
864
- </ToolbarButton>
865
- )}
866
- {resolvedFeatures.strike && (
867
- <ToolbarButton
868
- active={editor.isActive("strike")}
869
- disabled={!isEditable}
870
- title="Strikethrough"
871
- onClick={() => editor.chain().focus().toggleStrike().run()}
872
- >
873
- Strike
874
- </ToolbarButton>
875
- )}
876
- {resolvedFeatures.code && (
877
- <ToolbarButton
878
- active={editor.isActive("code")}
879
- disabled={!isEditable}
880
- title="Inline code"
881
- onClick={() => editor.chain().focus().toggleCode().run()}
882
- >
883
- Code
884
- </ToolbarButton>
885
- )}
886
- {resolvedFeatures.codeBlock && (
887
- <ToolbarButton
888
- active={editor.isActive("codeBlock")}
889
- disabled={!isEditable}
890
- title="Code block"
891
- onClick={() => editor.chain().focus().toggleCodeBlock().run()}
892
- >
893
- Code Block
894
- </ToolbarButton>
895
- )}
896
- </ToolbarGroup>
897
-
898
- <ToolbarGroup>
899
- {resolvedFeatures.bulletList && (
900
- <ToolbarButton
901
- active={editor.isActive("bulletList")}
902
- disabled={!isEditable}
903
- title="Bullet list"
904
- onClick={() =>
905
- editor.chain().focus().toggleBulletList().run()
906
- }
907
- >
908
- Bullet List
909
- </ToolbarButton>
910
- )}
911
- {resolvedFeatures.orderedList && (
912
- <ToolbarButton
913
- active={editor.isActive("orderedList")}
914
- disabled={!isEditable}
915
- title="Numbered list"
916
- onClick={() =>
917
- editor.chain().focus().toggleOrderedList().run()
918
- }
919
- >
920
- Numbered List
921
- </ToolbarButton>
922
- )}
923
- {resolvedFeatures.blockquote && (
924
- <ToolbarButton
925
- active={editor.isActive("blockquote")}
926
- disabled={!isEditable}
927
- title="Blockquote"
928
- onClick={() =>
929
- editor.chain().focus().toggleBlockquote().run()
930
- }
931
- >
932
- Quote
933
- </ToolbarButton>
934
- )}
935
- {resolvedFeatures.horizontalRule && (
936
- <ToolbarButton
937
- disabled={!isEditable}
938
- title="Horizontal rule"
939
- onClick={() =>
940
- editor.chain().focus().setHorizontalRule().run()
941
- }
942
- >
943
- Divider
944
- </ToolbarButton>
945
- )}
946
- </ToolbarGroup>
947
-
948
- {resolvedFeatures.align && (
949
- <ToolbarGroup>
950
- <ToolbarButton
951
- active={editor.isActive({ textAlign: "left" })}
952
- disabled={!isEditable}
953
- title="Align left"
954
- onClick={() =>
955
- editor.chain().focus().setTextAlign("left").run()
956
- }
957
- >
958
- Align Left
959
- </ToolbarButton>
960
- <ToolbarButton
961
- active={editor.isActive({ textAlign: "center" })}
962
- disabled={!isEditable}
963
- title="Align center"
964
- onClick={() =>
965
- editor.chain().focus().setTextAlign("center").run()
966
- }
967
- >
968
- Align Center
969
- </ToolbarButton>
970
- <ToolbarButton
971
- active={editor.isActive({ textAlign: "right" })}
972
- disabled={!isEditable}
973
- title="Align right"
974
- onClick={() =>
975
- editor.chain().focus().setTextAlign("right").run()
976
- }
977
- >
978
- Align Right
979
- </ToolbarButton>
980
- <ToolbarButton
981
- active={editor.isActive({ textAlign: "justify" })}
982
- disabled={!isEditable}
983
- title="Align justify"
984
- onClick={() =>
985
- editor.chain().focus().setTextAlign("justify").run()
986
- }
987
- >
988
- Justify
989
- </ToolbarButton>
990
- </ToolbarGroup>
991
- )}
992
-
993
- <ToolbarGroup>
994
- {allowLinks && (
995
- <Popover open={linkOpen} onOpenChange={setLinkOpen}>
996
- <PopoverTrigger
997
- render={
998
- <Button
999
- type="button"
1000
- variant="ghost"
1001
- size="xs"
1002
- disabled={!isEditable}
1003
- data-active={editor.isActive("link")}
1004
- >
1005
- Link
1006
- </Button>
1007
- }
1008
- />
1009
- <PopoverContent className="w-72">
1010
- <PopoverHeader>
1011
- <PopoverTitle>Insert link</PopoverTitle>
1012
- </PopoverHeader>
1013
- <div className="space-y-2">
1014
- <Input
1015
- value={linkUrl}
1016
- placeholder="https://example.com"
1017
- onChange={(event) => setLinkUrl(event.target.value)}
1018
- disabled={!isEditable}
1019
- />
1020
- <div className="flex justify-end gap-2">
1021
- <Button
1022
- type="button"
1023
- size="xs"
1024
- variant="outline"
1025
- onClick={handleRemoveLink}
1026
- disabled={!isEditable || !editor.isActive("link")}
1027
- >
1028
- Remove
1029
- </Button>
1030
- <Button
1031
- type="button"
1032
- size="xs"
1033
- onClick={handleApplyLink}
1034
- disabled={!isEditable}
1035
- >
1036
- Apply
1037
- </Button>
1038
- </div>
1039
- </div>
1040
- </PopoverContent>
1041
- </Popover>
1042
- )}
1043
-
1044
- {allowImages && (
1045
- <Popover open={imageOpen} onOpenChange={setImageOpen}>
1046
- <PopoverTrigger
1047
- render={
1048
- <Button
1049
- type="button"
1050
- variant="ghost"
1051
- size="xs"
1052
- disabled={!isEditable}
1053
- >
1054
- Image
1055
- </Button>
1056
- }
1057
- />
1058
- <PopoverContent className="w-80">
1059
- <PopoverHeader>
1060
- <PopoverTitle>Insert image</PopoverTitle>
1061
- </PopoverHeader>
1062
- <div className="space-y-3">
1063
- <div className="space-y-2">
1064
- <Input
1065
- value={imageUrl}
1066
- placeholder="https://example.com/image.jpg"
1067
- onChange={(event) => setImageUrl(event.target.value)}
1068
- disabled={!isEditable}
1069
- />
1070
- <Input
1071
- value={imageAlt}
1072
- placeholder="Alt text (optional)"
1073
- onChange={(event) => setImageAlt(event.target.value)}
1074
- disabled={!isEditable}
1075
- />
1076
- <div className="flex justify-end gap-2">
1077
- <Button
1078
- type="button"
1079
- size="xs"
1080
- onClick={handleInsertImageUrl}
1081
- disabled={!isEditable || !imageUrl}
1082
- >
1083
- Insert URL
1084
- </Button>
1085
- </div>
1086
- </div>
1087
-
1088
- {onImageUpload && (
1089
- <div className="space-y-2">
1090
- <div className="text-xs font-medium">Upload file</div>
1091
- <input
1092
- ref={fileInputRef}
1093
- type="file"
1094
- accept="image/*"
1095
- onChange={handleImageUpload}
1096
- className="sr-only"
1097
- disabled={!isEditable || uploadingImage}
1098
- />
1099
- <Button
1100
- type="button"
1101
- size="xs"
1102
- variant="outline"
1103
- onClick={() => fileInputRef.current?.click()}
1104
- disabled={!isEditable || uploadingImage}
1105
- >
1106
- {uploadingImage ? "Uploading..." : "Choose file"}
1107
- </Button>
1108
- </div>
1109
- )}
1110
- </div>
1111
- </PopoverContent>
1112
- </Popover>
1113
- )}
1114
-
1115
- {allowTableControls && (
1116
- <Popover>
1117
- <PopoverTrigger
1118
- render={
1119
- <Button
1120
- type="button"
1121
- variant="ghost"
1122
- size="xs"
1123
- disabled={!isEditable}
1124
- >
1125
- Table
1126
- </Button>
1127
- }
1128
- />
1129
- <PopoverContent className="w-80">
1130
- <PopoverHeader>
1131
- <PopoverTitle>Table tools</PopoverTitle>
1132
- </PopoverHeader>
1133
- <div className="grid grid-cols-2 gap-2">
1134
- <Button
1135
- type="button"
1136
- size="xs"
1137
- onClick={() =>
1138
- editor
1139
- .chain()
1140
- .focus()
1141
- .insertTable({
1142
- rows: 3,
1143
- cols: 3,
1144
- withHeaderRow: true,
1145
- })
1146
- .run()
1147
- }
1148
- disabled={!isEditable}
1149
- >
1150
- Insert table
1151
- </Button>
1152
- <Button
1153
- type="button"
1154
- size="xs"
1155
- variant="outline"
1156
- onClick={() =>
1157
- editor.chain().focus().addRowBefore().run()
1158
- }
1159
- disabled={!isEditable || !inTable}
1160
- >
1161
- Add row before
1162
- </Button>
1163
- <Button
1164
- type="button"
1165
- size="xs"
1166
- variant="outline"
1167
- onClick={() =>
1168
- editor.chain().focus().addRowAfter().run()
1169
- }
1170
- disabled={!isEditable || !inTable}
1171
- >
1172
- Add row after
1173
- </Button>
1174
- <Button
1175
- type="button"
1176
- size="xs"
1177
- variant="outline"
1178
- onClick={() =>
1179
- editor.chain().focus().addColumnBefore().run()
1180
- }
1181
- disabled={!isEditable || !inTable}
1182
- >
1183
- Add column before
1184
- </Button>
1185
- <Button
1186
- type="button"
1187
- size="xs"
1188
- variant="outline"
1189
- onClick={() =>
1190
- editor.chain().focus().addColumnAfter().run()
1191
- }
1192
- disabled={!isEditable || !inTable}
1193
- >
1194
- Add column after
1195
- </Button>
1196
- <Button
1197
- type="button"
1198
- size="xs"
1199
- variant="outline"
1200
- onClick={() => editor.chain().focus().deleteRow().run()}
1201
- disabled={!isEditable || !inTable}
1202
- >
1203
- Delete row
1204
- </Button>
1205
- <Button
1206
- type="button"
1207
- size="xs"
1208
- variant="outline"
1209
- onClick={() =>
1210
- editor.chain().focus().deleteColumn().run()
1211
- }
1212
- disabled={!isEditable || !inTable}
1213
- >
1214
- Delete column
1215
- </Button>
1216
- <Button
1217
- type="button"
1218
- size="xs"
1219
- variant="outline"
1220
- onClick={() =>
1221
- editor.chain().focus().toggleHeaderRow().run()
1222
- }
1223
- disabled={!isEditable || !inTable}
1224
- >
1225
- Toggle header row
1226
- </Button>
1227
- <Button
1228
- type="button"
1229
- size="xs"
1230
- variant="outline"
1231
- onClick={() =>
1232
- editor.chain().focus().toggleHeaderColumn().run()
1233
- }
1234
- disabled={!isEditable || !inTable}
1235
- >
1236
- Toggle header column
1237
- </Button>
1238
- <Button
1239
- type="button"
1240
- size="xs"
1241
- variant="outline"
1242
- onClick={() =>
1243
- editor.chain().focus().mergeCells().run()
1244
- }
1245
- disabled={!isEditable || !inTable}
1246
- >
1247
- Merge cells
1248
- </Button>
1249
- <Button
1250
- type="button"
1251
- size="xs"
1252
- variant="outline"
1253
- onClick={() => editor.chain().focus().splitCell().run()}
1254
- disabled={!isEditable || !inTable}
1255
- >
1256
- Split cell
1257
- </Button>
1258
- <Button
1259
- type="button"
1260
- size="xs"
1261
- variant="outline"
1262
- onClick={() =>
1263
- editor.chain().focus().deleteTable().run()
1264
- }
1265
- disabled={!isEditable || !inTable}
1266
- >
1267
- Delete table
1268
- </Button>
1269
- </div>
1270
- </PopoverContent>
1271
- </Popover>
1272
- )}
1273
- </ToolbarGroup>
1274
- </div>
1275
- )}
1276
-
1277
- {editor && allowBubbleMenu && (
1278
- <BubbleMenu
1279
- editor={editor}
1280
- className="flex items-center gap-1 rounded-md border bg-background p-1 shadow"
1281
- >
1282
- {resolvedFeatures.bold && (
1283
- <ToolbarButton
1284
- active={editor.isActive("bold")}
1285
- disabled={!isEditable}
1286
- title="Bold"
1287
- onClick={() => editor.chain().focus().toggleBold().run()}
1288
- >
1289
- Bold
1290
- </ToolbarButton>
1291
- )}
1292
- {resolvedFeatures.italic && (
1293
- <ToolbarButton
1294
- active={editor.isActive("italic")}
1295
- disabled={!isEditable}
1296
- title="Italic"
1297
- onClick={() => editor.chain().focus().toggleItalic().run()}
1298
- >
1299
- Italic
1300
- </ToolbarButton>
1301
- )}
1302
- {resolvedFeatures.underline && (
1303
- <ToolbarButton
1304
- active={editor.isActive("underline")}
1305
- disabled={!isEditable}
1306
- title="Underline"
1307
- onClick={() => editor.chain().focus().toggleUnderline().run()}
1308
- >
1309
- Underline
1310
- </ToolbarButton>
1311
- )}
1312
- </BubbleMenu>
1313
- )}
1314
-
1315
- <EditorContent editor={editor} id={name} />
1316
-
1317
- {allowCharacterCount && showCharacterCount && (
1318
- <div className="flex items-center justify-between border-t bg-muted/30 px-2 py-1 text-xs text-muted-foreground">
1319
- <span>
1320
- {characterCount.words} word{characterCount.words === 1 ? "" : "s"}
1321
- </span>
1322
- <span>
1323
- {characterCount.characters}
1324
- {typeof maxCharacters === "number" ? ` / ${maxCharacters}` : ""}
1325
- characters
1326
- </span>
1327
- </div>
1328
- )}
1329
- </div>
1330
-
1331
- {description && (
1332
- <p className="text-muted-foreground text-xs">{description}</p>
1333
- )}
1334
- {error && <p className="text-destructive text-xs">{error}</p>}
1335
- </div>
1336
- );
1337
- }