@lark-apaas/coding-template-nestjs-react-fullstack 0.1.35 → 0.1.36-alpha.20260901035538

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 (33) hide show
  1. package/package.json +1 -1
  2. package/template/client/src/components/ui/markdown.tsx +43 -0
  3. package/template/client/src/index.css +0 -1
  4. package/template/nest-cli.json +10 -9
  5. package/template/package-lock.json +10339 -18979
  6. package/template/package.json +7 -70
  7. package/template/scripts/build.sh +12 -0
  8. package/template/vite.config.ts +1 -1
  9. package/template/client/src/components/business-ui/tiptap-editor/README.md +0 -324
  10. package/template/client/src/components/business-ui/tiptap-editor/components/attachment-toolbar-button.tsx +0 -74
  11. package/template/client/src/components/business-ui/tiptap-editor/components/blockquote-toolbar-button.tsx +0 -41
  12. package/template/client/src/components/business-ui/tiptap-editor/components/code-block-toolbar-button.tsx +0 -41
  13. package/template/client/src/components/business-ui/tiptap-editor/components/color-highlight-toolbar-button.tsx +0 -141
  14. package/template/client/src/components/business-ui/tiptap-editor/components/heading-toolbar-button.tsx +0 -99
  15. package/template/client/src/components/business-ui/tiptap-editor/components/horizontal-rule-toolbar-button.tsx +0 -39
  16. package/template/client/src/components/business-ui/tiptap-editor/components/image-upload-toolbar-button.tsx +0 -65
  17. package/template/client/src/components/business-ui/tiptap-editor/components/link-edit-form.tsx +0 -127
  18. package/template/client/src/components/business-ui/tiptap-editor/components/link-hover-toolbar.tsx +0 -380
  19. package/template/client/src/components/business-ui/tiptap-editor/components/link-toolbar-button.tsx +0 -96
  20. package/template/client/src/components/business-ui/tiptap-editor/components/list-toolbar-button.tsx +0 -75
  21. package/template/client/src/components/business-ui/tiptap-editor/components/mark-toolbar-button.tsx +0 -118
  22. package/template/client/src/components/business-ui/tiptap-editor/components/text-align-toolbar-button.tsx +0 -84
  23. package/template/client/src/components/business-ui/tiptap-editor/components/undo-redo-toolbar-button.tsx +0 -54
  24. package/template/client/src/components/business-ui/tiptap-editor/extensions/attachment.tsx +0 -539
  25. package/template/client/src/components/business-ui/tiptap-editor/extensions/code-block-shiki.tsx +0 -236
  26. package/template/client/src/components/business-ui/tiptap-editor/extensions/complete-kit.ts +0 -260
  27. package/template/client/src/components/business-ui/tiptap-editor/extensions/image.tsx +0 -456
  28. package/template/client/src/components/business-ui/tiptap-editor/hooks/use-tiptap-editor.ts +0 -47
  29. package/template/client/src/components/business-ui/tiptap-editor/index.ts +0 -38
  30. package/template/client/src/components/business-ui/tiptap-editor/tiptap-editor-complete.tsx +0 -112
  31. package/template/client/src/components/business-ui/tiptap-editor/tiptap-editor.tsx +0 -169
  32. package/template/client/src/components/ui/streamdown.tsx +0 -186
  33. package/template/client/src/lib/shiki.ts +0 -92
@@ -1,456 +0,0 @@
1
- 'use client';
2
-
3
- import * as React from 'react';
4
- import { mergeAttributes, type CommandProps } from '@tiptap/core';
5
- import TiptapImage from '@tiptap/extension-image';
6
- import { type Node as ProseMirrorNode } from '@tiptap/pm/model';
7
- import {
8
- NodeViewWrapper,
9
- ReactNodeViewRenderer,
10
- type NodeViewProps,
11
- } from '@tiptap/react';
12
- import { nanoid } from 'nanoid';
13
-
14
- import { uploadFile } from '@/components/business-ui/api/files/service';
15
- import { cn } from '@/lib/utils';
16
-
17
- export interface ImageAttrs {
18
- src: string;
19
- alt?: string;
20
- title?: string;
21
- width?: number | string | null;
22
- height?: number | null;
23
- uploadId?: string | null;
24
- }
25
-
26
- export type ImageUploadFn = (file: File) => Promise<string>;
27
-
28
- export interface ImageOptions {
29
- upload?: ImageUploadFn;
30
- HTMLAttributes?: Record<string, unknown>;
31
- }
32
-
33
- declare module '@tiptap/core' {
34
- interface Commands<ReturnType> {
35
- imageUpload: {
36
- insertImages: (files: File[]) => ReturnType;
37
- };
38
- }
39
- }
40
-
41
- function findImageByUploadId(doc: ProseMirrorNode, uploadId: string) {
42
- let foundPos: number | null = null;
43
- doc.descendants((node: ProseMirrorNode, pos: number) => {
44
- if (foundPos != null) return false;
45
- if (node.type?.name === 'image' && node.attrs?.uploadId === uploadId) {
46
- foundPos = pos;
47
- return false;
48
- }
49
- return true;
50
- });
51
- return foundPos;
52
- }
53
-
54
- function clampSize(value: number, { min, max }: { min: number; max: number }) {
55
- if (!Number.isFinite(value)) return min;
56
- return Math.max(min, Math.min(max, value));
57
- }
58
-
59
- function ImageNodeView(props: NodeViewProps) {
60
- const { editor, selected } = props;
61
- const attrs = props.node.attrs as ImageAttrs;
62
- const wrapperRef = React.useRef<HTMLDivElement>(null);
63
- const imgRef = React.useRef<HTMLImageElement>(null);
64
- const [naturalRatio, setNaturalRatio] = React.useState<number | null>(null);
65
-
66
- const isEditable = editor.isEditable;
67
-
68
- // 计算宽高比:优先使用属性中的宽高,否则使用自然宽高比
69
- const aspectRatio =
70
- typeof attrs.width === 'number' && attrs.height
71
- ? attrs.width / attrs.height
72
- : naturalRatio;
73
-
74
- const style = React.useMemo<React.CSSProperties>(() => {
75
- if (attrs.width === '100%') {
76
- return {
77
- width: '100%',
78
- height: 'auto',
79
- };
80
- }
81
- if (typeof attrs.width === 'number' && attrs.height) {
82
- return {
83
- width: `${attrs.width}px`,
84
- aspectRatio: `${attrs.width} / ${attrs.height}`,
85
- };
86
- }
87
- return {};
88
- }, [attrs.width, attrs.height]);
89
-
90
- const startResize = React.useCallback(
91
- (corner: 'nw' | 'ne' | 'sw' | 'se') =>
92
- (e: React.PointerEvent<HTMLButtonElement>) => {
93
- if (!isEditable) return;
94
- e.preventDefault();
95
- e.stopPropagation();
96
-
97
- const img = imgRef.current;
98
- const wrapper = wrapperRef.current;
99
- if (!img || !wrapper) return;
100
-
101
- const rect = img.getBoundingClientRect();
102
- const startWidth = rect.width;
103
- const startHeight = rect.height;
104
-
105
- const ratio =
106
- aspectRatio ?? (startHeight ? startWidth / startHeight : null);
107
- if (!ratio) return;
108
-
109
- const startX = e.clientX;
110
- const startY = e.clientY;
111
-
112
- const containerWidth = wrapper.parentElement?.clientWidth ?? rect.width;
113
- const minWidth = 80;
114
-
115
- const onMove = (ev: PointerEvent) => {
116
- const dx = ev.clientX - startX;
117
- const dy = ev.clientY - startY;
118
-
119
- // 保持宽高比
120
- // 根据角落方向调整宽度
121
- let nextWidth = startWidth;
122
- if (corner === 'ne' || corner === 'se') {
123
- nextWidth = startWidth + dx;
124
- } else {
125
- nextWidth = startWidth - dx;
126
- }
127
-
128
- // 允许垂直拖拽调整大小,根据角落映射 dy 到宽度变化
129
- const dyAsWidth = dy * ratio;
130
- if (corner === 'se' || corner === 'sw') {
131
- nextWidth =
132
- Math.abs(dyAsWidth) > Math.abs(dx)
133
- ? startWidth + dyAsWidth
134
- : nextWidth;
135
- } else {
136
- nextWidth =
137
- Math.abs(dyAsWidth) > Math.abs(dx)
138
- ? startWidth - dyAsWidth
139
- : nextWidth;
140
- }
141
-
142
- nextWidth = clampSize(nextWidth, {
143
- min: minWidth,
144
- max: containerWidth,
145
- });
146
-
147
- const nextHeight = nextWidth / ratio;
148
-
149
- // 如果接近容器宽度,吸附到 100%
150
- if (Math.abs(containerWidth - nextWidth) < 10) {
151
- props.updateAttributes({
152
- width: '100%',
153
- height: null,
154
- });
155
- return;
156
- }
157
-
158
- props.updateAttributes({
159
- width: Math.round(nextWidth),
160
- height: Math.round(nextHeight),
161
- });
162
- };
163
-
164
- const onUp = () => {
165
- window.removeEventListener('pointermove', onMove);
166
- window.removeEventListener('pointerup', onUp);
167
- };
168
-
169
- window.addEventListener('pointermove', onMove);
170
- window.addEventListener('pointerup', onUp);
171
- },
172
- [aspectRatio, isEditable, props],
173
- );
174
-
175
- return (
176
- <NodeViewWrapper
177
- className={cn('not-prose my-3 flex justify-center')}
178
- data-type="image-block"
179
- >
180
- <div
181
- ref={wrapperRef}
182
- className={cn(
183
- 'relative inline-block max-w-full rounded-md',
184
- selected && isEditable ? 'ring-2 ring-ring/50' : '',
185
- )}
186
- style={style}
187
- contentEditable={false}
188
- >
189
- <img
190
- ref={imgRef}
191
- src={attrs.src}
192
- alt={attrs.alt ?? ''}
193
- title={attrs.title}
194
- className={cn('block size-full rounded-md object-cover')}
195
- draggable={false}
196
- onLoad={(e) => {
197
- const img = e.currentTarget;
198
- if (img.naturalWidth && img.naturalHeight) {
199
- setNaturalRatio(img.naturalWidth / img.naturalHeight);
200
- }
201
- }}
202
- />
203
-
204
- {attrs.uploadId ? (
205
- <>
206
- <div className="pointer-events-none absolute inset-0 rounded-md bg-black/25" />
207
- <div className="pointer-events-none absolute top-2 right-2 flex items-center gap-1 rounded-md bg-primary-foreground/20 px-2 py-1 text-sm/5 text-primary-foreground">
208
- <span
209
- className="size-4 animate-spin rounded-full border-2 border-primary-foreground border-t-transparent"
210
- aria-hidden="true"
211
- />
212
- <span className="sr-only">图片上传中</span>
213
- </div>
214
- </>
215
- ) : null}
216
-
217
- {selected && isEditable && (
218
- <>
219
- <ResizeHandle corner="nw" onPointerDown={startResize('nw')} />
220
- <ResizeHandle corner="ne" onPointerDown={startResize('ne')} />
221
- <ResizeHandle corner="sw" onPointerDown={startResize('sw')} />
222
- <ResizeHandle corner="se" onPointerDown={startResize('se')} />
223
- </>
224
- )}
225
- </div>
226
- </NodeViewWrapper>
227
- );
228
- }
229
-
230
- function ResizeHandle({
231
- corner,
232
- onPointerDown,
233
- }: {
234
- corner: 'nw' | 'ne' | 'sw' | 'se';
235
- onPointerDown: (e: React.PointerEvent<HTMLButtonElement>) => void;
236
- }) {
237
- const positionClass =
238
- corner === 'nw'
239
- ? '-left-1.5 -top-1.5'
240
- : corner === 'ne'
241
- ? '-right-1.5 -top-1.5'
242
- : corner === 'sw'
243
- ? '-left-1.5 -bottom-1.5'
244
- : '-right-1.5 -bottom-1.5';
245
-
246
- const cursorClass =
247
- corner === 'nw' || corner === 'se'
248
- ? 'cursor-nwse-resize'
249
- : 'cursor-nesw-resize';
250
-
251
- const label =
252
- corner === 'nw'
253
- ? 'Resize image from top-left'
254
- : corner === 'ne'
255
- ? 'Resize image from top-right'
256
- : corner === 'sw'
257
- ? 'Resize image from bottom-left'
258
- : 'Resize image from bottom-right';
259
-
260
- return (
261
- <button
262
- type="button"
263
- aria-label={label}
264
- className={cn(
265
- 'absolute z-10 flex size-2.5 items-center justify-center rounded-full border border-primary bg-background shadow-sm',
266
- cursorClass,
267
- positionClass,
268
- )}
269
- onPointerDown={onPointerDown}
270
- />
271
- );
272
- }
273
-
274
- export const Image = TiptapImage.extend<ImageOptions>({
275
- addOptions() {
276
- return {
277
- upload: async (file: File) => {
278
- const data = await uploadFile(file);
279
- return data.url;
280
- },
281
- };
282
- },
283
-
284
- addAttributes() {
285
- return {
286
- ...this.parent?.(),
287
- width: {
288
- default: null,
289
- parseHTML: (element) => {
290
- const styleWidth = element.style?.width;
291
- if (styleWidth && styleWidth.trim().endsWith('%')) {
292
- return styleWidth.trim();
293
- }
294
-
295
- const rawWidth = element.getAttribute('width');
296
- if (!rawWidth) return null;
297
- if (rawWidth.trim().endsWith('%')) return rawWidth.trim();
298
-
299
- const parsed = Number.parseInt(rawWidth, 10);
300
- return Number.isFinite(parsed) ? parsed : null;
301
- },
302
- renderHTML: (attributes) => {
303
- const width = attributes.width;
304
- if (!width) return {};
305
- if (width === '100%') {
306
- return {
307
- style: 'width: 100%; height: auto;',
308
- };
309
- }
310
- if (typeof width === 'number') {
311
- return { width: String(width) };
312
- }
313
- return {
314
- style: `width: ${String(width)}; height: auto;`,
315
- };
316
- },
317
- },
318
- height: {
319
- default: null,
320
- parseHTML: (element) => {
321
- const rawHeight = element.getAttribute('height');
322
- if (!rawHeight) return null;
323
- const parsed = Number.parseInt(rawHeight, 10);
324
- return Number.isFinite(parsed) ? parsed : null;
325
- },
326
- renderHTML: (attributes) => {
327
- const height = attributes.height;
328
- if (!height) return {};
329
- return { height: String(height) };
330
- },
331
- },
332
- uploadId: {
333
- default: null,
334
- rendered: false,
335
- },
336
- };
337
- },
338
-
339
- renderHTML({ HTMLAttributes }) {
340
- // Width/height serialization happens via addAttributes().
341
- // We intentionally do not put layout classes on the wrapper; consumers can style by data-type.
342
- return [
343
- 'div',
344
- {
345
- 'data-type': 'image-block',
346
- },
347
- [
348
- 'img',
349
- mergeAttributes(
350
- this.options.HTMLAttributes ?? {},
351
- Object.fromEntries(Object.entries(HTMLAttributes)),
352
- ),
353
- ],
354
- ];
355
- },
356
-
357
- parseHTML() {
358
- return [
359
- {
360
- tag: 'div[data-type="image-block"] img[src]',
361
- },
362
- {
363
- tag: 'img[src]:not([src^="data:"])',
364
- },
365
- ];
366
- },
367
-
368
- addCommands() {
369
- return {
370
- ...this.parent?.(),
371
- insertImages:
372
- (files: File[]) =>
373
- ({ editor, commands }: CommandProps) => {
374
- if (!files || files.length === 0) return false;
375
-
376
- const upload = this.options.upload;
377
- if (!upload) return false;
378
-
379
- const uploads = files.map((file) => {
380
- const uploadId = nanoid();
381
- const previewUrl = URL.createObjectURL(file);
382
-
383
- return {
384
- file,
385
- uploadId,
386
- previewUrl,
387
- };
388
- });
389
-
390
- // 一次性插入多个节点,避免 NodeSelection 导致后续插入替换掉前一个节点
391
- const inserted = commands.insertContent([
392
- ...uploads.map(({ uploadId, previewUrl }) => ({
393
- type: this.name,
394
- attrs: {
395
- src: previewUrl,
396
- uploadId,
397
- },
398
- })),
399
- { type: 'paragraph' },
400
- ]);
401
-
402
- // 并发上传:不阻塞命令返回
403
- void Promise.all(
404
- uploads.map(async ({ file, uploadId, previewUrl }) => {
405
- try {
406
- const url = await upload(file);
407
-
408
- // Ensure state is settled
409
- await new Promise((resolve) => requestAnimationFrame(resolve));
410
-
411
- const pos = findImageByUploadId(editor.state.doc, uploadId);
412
- if (pos == null) return;
413
-
414
- const node = editor.state.doc.nodeAt(pos);
415
- if (!node || node.type.name !== this.name) return;
416
-
417
- editor
418
- .chain()
419
- .command(({ tr }: CommandProps) => {
420
- tr.setNodeMarkup(pos, undefined, {
421
- ...node.attrs,
422
- src: url,
423
- uploadId: null,
424
- });
425
- return true;
426
- })
427
- .run();
428
- } catch {
429
- const pos = findImageByUploadId(editor.state.doc, uploadId);
430
- if (pos != null) {
431
- editor
432
- .chain()
433
- .deleteRange({ from: pos, to: pos + 1 })
434
- .run();
435
- }
436
- } finally {
437
- if (previewUrl.startsWith('blob:')) {
438
- try {
439
- URL.revokeObjectURL(previewUrl);
440
- } catch {
441
- // ignore
442
- }
443
- }
444
- }
445
- }),
446
- );
447
-
448
- return inserted;
449
- },
450
- };
451
- },
452
-
453
- addNodeView() {
454
- return ReactNodeViewRenderer(ImageNodeView);
455
- },
456
- });
@@ -1,47 +0,0 @@
1
- import * as React from 'react';
2
- import { useCurrentEditor, useEditorState, type Editor } from '@tiptap/react';
3
-
4
- /**
5
- * Hook that provides access to a Tiptap editor instance.
6
- *
7
- * Accepts an optional editor instance directly, or falls back to retrieving
8
- * the editor from the Tiptap context if available. This allows components
9
- * to work both when given an editor directly and when used within a Tiptap
10
- * editor context.
11
- *
12
- * @param providedEditor - Optional editor instance to use instead of the context editor
13
- * @returns The provided editor or the editor from context, whichever is available
14
- */
15
- export function useTiptapEditor(providedEditor?: Editor | null): {
16
- editor: Editor | null;
17
- editorState?: Editor['state'];
18
- canCommand?: Editor['can'];
19
- } {
20
- const { editor: coreEditor } = useCurrentEditor();
21
-
22
- const mainEditor = React.useMemo(
23
- () => providedEditor || coreEditor,
24
- [providedEditor, coreEditor],
25
- );
26
-
27
- const editorState = useEditorState({
28
- editor: mainEditor,
29
- selector(context) {
30
- if (!context.editor) {
31
- return {
32
- editor: null,
33
- editorState: undefined,
34
- canCommand: undefined,
35
- };
36
- }
37
-
38
- return {
39
- editor: context.editor,
40
- editorState: context.editor.state,
41
- canCommand: context.editor.can.bind(context.editor),
42
- };
43
- },
44
- });
45
-
46
- return editorState || { editor: null };
47
- }
@@ -1,38 +0,0 @@
1
- // #region 完整版组件
2
- export {
3
- TiptapEditorComplete,
4
- type TiptapEditorCompleteProps,
5
- } from '@/components/business-ui/tiptap-editor/tiptap-editor-complete';
6
- // #endregion
7
-
8
- // #region 组合式 API
9
- export {
10
- TiptapEditor,
11
- TiptapEditorContent,
12
- TiptapEditorToolbar,
13
- TiptapEditorToolbarSeparator,
14
- type TiptapEditorProps,
15
- } from '@/components/business-ui/tiptap-editor/tiptap-editor';
16
- // #endregion
17
-
18
- // #region 扩展
19
- export {
20
- CompleteKit,
21
- type CompleteKitOptions,
22
- } from '@/components/business-ui/tiptap-editor/extensions/complete-kit';
23
- // #endregion
24
-
25
- // #region 工具栏可用工具按钮
26
- export { AttachmentToolbarButton } from '@/components/business-ui/tiptap-editor/components/attachment-toolbar-button';
27
- export { BlockquoteToolbarButton } from '@/components/business-ui/tiptap-editor/components/blockquote-toolbar-button';
28
- export { CodeBlockToolbarButton } from '@/components/business-ui/tiptap-editor/components/code-block-toolbar-button';
29
- export { ColorHighlightToolbarButton } from '@/components/business-ui/tiptap-editor/components/color-highlight-toolbar-button';
30
- export { HeadingToolbarButton } from '@/components/business-ui/tiptap-editor/components/heading-toolbar-button';
31
- export { HorizontalRuleToolbarButton } from '@/components/business-ui/tiptap-editor/components/horizontal-rule-toolbar-button';
32
- export { ImageUploadToolbarButton } from '@/components/business-ui/tiptap-editor/components/image-upload-toolbar-button';
33
- export { LinkToolbarButton } from '@/components/business-ui/tiptap-editor/components/link-toolbar-button';
34
- export { ListToolbarButton } from '@/components/business-ui/tiptap-editor/components/list-toolbar-button';
35
- export { MarkToolbarButton } from '@/components/business-ui/tiptap-editor/components/mark-toolbar-button';
36
- export { TextAlignToolbarButton } from '@/components/business-ui/tiptap-editor/components/text-align-toolbar-button';
37
- export { UndoRedoToolbarButton } from '@/components/business-ui/tiptap-editor/components/undo-redo-toolbar-button';
38
- // #endregion
@@ -1,112 +0,0 @@
1
- 'use client';
2
-
3
- import { AttachmentToolbarButton } from '@/components/business-ui/tiptap-editor/components/attachment-toolbar-button';
4
- import { BlockquoteToolbarButton } from '@/components/business-ui/tiptap-editor/components/blockquote-toolbar-button';
5
- import { CodeBlockToolbarButton } from '@/components/business-ui/tiptap-editor/components/code-block-toolbar-button';
6
- import { ColorHighlightToolbarButton } from '@/components/business-ui/tiptap-editor/components/color-highlight-toolbar-button';
7
- import { HeadingToolbarButton } from '@/components/business-ui/tiptap-editor/components/heading-toolbar-button';
8
- import { HorizontalRuleToolbarButton } from '@/components/business-ui/tiptap-editor/components/horizontal-rule-toolbar-button';
9
- import { ImageUploadToolbarButton } from '@/components/business-ui/tiptap-editor/components/image-upload-toolbar-button';
10
- import { LinkToolbarButton } from '@/components/business-ui/tiptap-editor/components/link-toolbar-button';
11
- import { ListToolbarButton } from '@/components/business-ui/tiptap-editor/components/list-toolbar-button';
12
- import { MarkToolbarButton } from '@/components/business-ui/tiptap-editor/components/mark-toolbar-button';
13
- import { TextAlignToolbarButton } from '@/components/business-ui/tiptap-editor/components/text-align-toolbar-button';
14
- import { UndoRedoToolbarButton } from '@/components/business-ui/tiptap-editor/components/undo-redo-toolbar-button';
15
- import { CompleteKit } from '@/components/business-ui/tiptap-editor/extensions/complete-kit';
16
- import {
17
- TiptapEditor,
18
- TiptapEditorContent,
19
- TiptapEditorToolbar,
20
- TiptapEditorToolbarSeparator,
21
- type TiptapEditorProps,
22
- } from '@/components/business-ui/tiptap-editor/tiptap-editor';
23
- import { cn } from '@/lib/utils';
24
-
25
- export interface TiptapEditorCompleteProps extends Omit<
26
- TiptapEditorProps,
27
- 'extensions'
28
- > {
29
- /** 占位符文本 */
30
- placeholder?: string;
31
- }
32
-
33
- /**
34
- * 一个完整、开箱即用的富文本编辑器组件,包含预配置的工具栏。
35
- * 这是大多数使用场景下的推荐组件。
36
- *
37
- * 功能特性:
38
- * - 预配置的工具栏,包含常用的格式化选项
39
- * - 已内置完整样式,包括边框、焦点态等,你无需在此编辑器外包裹 div 实现样式
40
- * - 支持撤销/重做
41
- * - 标题样式 (H1-H6)
42
- * - 文本对齐
43
- * - 列表 (无序、有序、任务)
44
- * - 文本格式化 (加粗、斜体、下划线、删除线、代码)
45
- *
46
- * @example
47
- * ```tsx
48
- * <TiptapEditorComplete
49
- * value={value}
50
- * onValueChange={setValue}
51
- * placeholder="在此输入消息..."
52
- * aria-invalid={invalid}
53
- * aria-disabled={disabled}
54
- * />
55
- * ```
56
- */
57
- export function TiptapEditorComplete({
58
- className,
59
- placeholder,
60
- ...props
61
- }: TiptapEditorCompleteProps) {
62
- const extensions = [CompleteKit.configure({ placeholder: { placeholder } })];
63
-
64
- return (
65
- <TiptapEditor
66
- className={cn('max-h-140 min-h-80', className)}
67
- extensions={extensions}
68
- {...props}
69
- >
70
- <DefaultToolbar />
71
- <TiptapEditorContent />
72
- </TiptapEditor>
73
- );
74
- }
75
-
76
- function DefaultToolbar() {
77
- return (
78
- <TiptapEditorToolbar>
79
- {/* 撤销/重做 */}
80
- <UndoRedoToolbarButton action="undo" />
81
- <UndoRedoToolbarButton action="redo" />
82
- <TiptapEditorToolbarSeparator />
83
-
84
- {/* 标题/正文 */}
85
- <HeadingToolbarButton />
86
- <TiptapEditorToolbarSeparator />
87
-
88
- {/* 文本对齐 & 列表 */}
89
- <TextAlignToolbarButton />
90
- <ListToolbarButton />
91
- <TiptapEditorToolbarSeparator />
92
-
93
- {/* 文本格式化: 加粗、删除线、斜体、下划线、颜色 */}
94
- <MarkToolbarButton format="bold" />
95
- <MarkToolbarButton format="strike" />
96
- <MarkToolbarButton format="italic" />
97
- <MarkToolbarButton format="underline" />
98
- <ColorHighlightToolbarButton />
99
- <TiptapEditorToolbarSeparator />
100
-
101
- {/* 块级元素: 引用、分割线、代码块 */}
102
- <BlockquoteToolbarButton />
103
- <HorizontalRuleToolbarButton />
104
- <CodeBlockToolbarButton />
105
-
106
- {/* 链接 & 媒体: 图片、附件 */}
107
- <LinkToolbarButton />
108
- <ImageUploadToolbarButton />
109
- <AttachmentToolbarButton />
110
- </TiptapEditorToolbar>
111
- );
112
- }