@lark-apaas/coding-template-nestjs-react-fullstack 0.1.36-alpha.20260902024923 → 0.1.36-alpha.20260902103327

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 (31) hide show
  1. package/package.json +1 -1
  2. package/template/client/src/components/business-ui/tiptap-editor/README.md +324 -0
  3. package/template/client/src/components/business-ui/tiptap-editor/components/attachment-toolbar-button.tsx +74 -0
  4. package/template/client/src/components/business-ui/tiptap-editor/components/blockquote-toolbar-button.tsx +41 -0
  5. package/template/client/src/components/business-ui/tiptap-editor/components/code-block-toolbar-button.tsx +41 -0
  6. package/template/client/src/components/business-ui/tiptap-editor/components/color-highlight-toolbar-button.tsx +141 -0
  7. package/template/client/src/components/business-ui/tiptap-editor/components/heading-toolbar-button.tsx +99 -0
  8. package/template/client/src/components/business-ui/tiptap-editor/components/horizontal-rule-toolbar-button.tsx +39 -0
  9. package/template/client/src/components/business-ui/tiptap-editor/components/image-upload-toolbar-button.tsx +65 -0
  10. package/template/client/src/components/business-ui/tiptap-editor/components/link-edit-form.tsx +127 -0
  11. package/template/client/src/components/business-ui/tiptap-editor/components/link-hover-toolbar.tsx +380 -0
  12. package/template/client/src/components/business-ui/tiptap-editor/components/link-toolbar-button.tsx +96 -0
  13. package/template/client/src/components/business-ui/tiptap-editor/components/list-toolbar-button.tsx +75 -0
  14. package/template/client/src/components/business-ui/tiptap-editor/components/mark-toolbar-button.tsx +118 -0
  15. package/template/client/src/components/business-ui/tiptap-editor/components/text-align-toolbar-button.tsx +84 -0
  16. package/template/client/src/components/business-ui/tiptap-editor/components/undo-redo-toolbar-button.tsx +54 -0
  17. package/template/client/src/components/business-ui/tiptap-editor/extensions/attachment.tsx +539 -0
  18. package/template/client/src/components/business-ui/tiptap-editor/extensions/code-block-shiki.tsx +236 -0
  19. package/template/client/src/components/business-ui/tiptap-editor/extensions/complete-kit.ts +260 -0
  20. package/template/client/src/components/business-ui/tiptap-editor/extensions/image.tsx +456 -0
  21. package/template/client/src/components/business-ui/tiptap-editor/hooks/use-tiptap-editor.ts +47 -0
  22. package/template/client/src/components/business-ui/tiptap-editor/index.ts +38 -0
  23. package/template/client/src/components/business-ui/tiptap-editor/tiptap-editor-complete.tsx +112 -0
  24. package/template/client/src/components/business-ui/tiptap-editor/tiptap-editor.tsx +169 -0
  25. package/template/client/src/components/ui/streamdown.tsx +186 -0
  26. package/template/client/src/index.css +1 -0
  27. package/template/client/src/lib/shiki.ts +92 -0
  28. package/template/nest-cli.json +9 -10
  29. package/template/package-lock.json +18307 -9660
  30. package/template/package.json +68 -5
  31. package/template/client/src/components/ui/markdown.tsx +0 -43
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lark-apaas/coding-template-nestjs-react-fullstack",
3
- "version": "0.1.36-alpha.20260902024923",
3
+ "version": "0.1.36-alpha.20260902103327",
4
4
  "description": "NestJS + React fullstack template for miaoda coding",
5
5
  "type": "module",
6
6
  "files": [
@@ -0,0 +1,324 @@
1
+ # Tiptap Editor
2
+
3
+ > 基于 [Tiptap](https://tiptap.dev) 构建的富文本编辑器组件,提供开箱即用和可组合两种使用模式。
4
+
5
+ ## 快速开始
6
+
7
+ ### 推荐:使用 `TiptapEditorComplete`
8
+
9
+ 对于大多数场景,直接使用 `TiptapEditorComplete` 即可。它封装了完整的工具栏和样式,开箱即用。
10
+
11
+ ```tsx
12
+ import { TiptapEditorComplete } from '@/components/business-ui/tiptap-editor';
13
+
14
+ function MyEditor() {
15
+ const [value, setValue] = React.useState('');
16
+
17
+ return (
18
+ <TiptapEditorComplete
19
+ value={value}
20
+ onValueChange={setValue}
21
+ placeholder="请输入内容..."
22
+ />
23
+ );
24
+ }
25
+ ```
26
+
27
+ #### `TiptapEditorComplete` Props
28
+
29
+ | Prop | 类型 | 默认值 | 描述 |
30
+ | :-------------- | :------------------------ | :----- | :----------------------- |
31
+ | `value` | `string` | - | 受控值 (HTML 字符串) |
32
+ | `defaultValue` | `string` | - | 非受控默认值 |
33
+ | `onValueChange` | `(value: string) => void` | - | 内容变更回调 |
34
+ | `placeholder` | `string` | - | 占位符文本 |
35
+ | `readOnly` | `boolean` | false | 只读模式 |
36
+ | `aria-disabled` | `boolean` | - | 禁用状态 |
37
+ | `aria-invalid` | `boolean` | - | 无效状态(用于表单校验) |
38
+ | `className` | `string` | - | 自定义样式类 |
39
+
40
+ #### ⚠️ 样式注意事项
41
+
42
+ **组件已包含完整的边框、内边距、Hover 边框、聚焦环等等丰富且完整的视觉样式,请勿在外层添加额外包装!**
43
+
44
+ ```tsx
45
+ // ❌ 错误:不要用 div 包裹并添加 border、padding、overflow-auto 等样式
46
+ // 例如这段代码会导致展示多层 border,编辑器聚焦时聚焦环被截断等问题
47
+ <div className="border rounded-lg overflow-auto">
48
+ <TiptapEditorComplete ... />
49
+ </div>
50
+
51
+ // ✅ 正确:直接使用,只建议添加 w-full、h-auto 等尺寸类
52
+ // 完整版组件默认具有 max-h-140 min-h-80,必要时可覆盖。
53
+ <TiptapEditorComplete className="w-full max-h-100" ... />
54
+ ```
55
+
56
+ ---
57
+
58
+ ## 组合式 API(高级用法)
59
+
60
+ > ⚠️ **警告**:必须优先使用 `TiptapEditorComplete`!仅当用户**明确要求自定义工具栏**或**添加特殊功能**时,才使用组合式 API。绝大多数场景下,`TiptapEditorComplete` 已能满足需求。
61
+
62
+ 当需要 **自定义工具栏布局** 或 **添加特殊功能** 时,使用组合式 API。
63
+
64
+ ```tsx
65
+ import {
66
+ TiptapEditor,
67
+ TiptapEditorContent,
68
+ TiptapEditorToolbar,
69
+ TiptapEditorToolbarSeparator,
70
+ UndoRedoToolbarButton,
71
+ MarkToolbarButton,
72
+ HeadingToolbarButton,
73
+ ListToolbarButton,
74
+ TextAlignToolbarButton,
75
+ ColorHighlightToolbarButton,
76
+ BlockquoteToolbarButton,
77
+ HorizontalRuleToolbarButton,
78
+ CodeBlockToolbarButton,
79
+ LinkToolbarButton,
80
+ ImageUploadToolbarButton,
81
+ AttachmentToolbarButton,
82
+ CompleteKit,
83
+ } from '@/components/business-ui/tiptap-editor';
84
+
85
+ function CustomEditor() {
86
+ // placeholder 需要通过 CompleteKit 配置
87
+ const extensions = [CompleteKit.configure({ placeholder: { placeholder: '请输入内容...' } })];
88
+
89
+ return (
90
+ <TiptapEditor extensions={extensions}>
91
+ <CustomToolbar />
92
+ <TiptapEditorContent />
93
+ </TiptapEditor>
94
+ );
95
+ }
96
+
97
+ /** 自定义工具栏:可根据需求自由组合按钮 */
98
+ function CustomToolbar() {
99
+ return (
100
+ <TiptapEditorToolbar>
101
+ {/* 撤销/重做 */}
102
+ <UndoRedoToolbarButton action="undo" />
103
+ <UndoRedoToolbarButton action="redo" />
104
+ <TiptapEditorToolbarSeparator />
105
+
106
+ {/* 标题/正文 */}
107
+ <HeadingToolbarButton />
108
+ <TiptapEditorToolbarSeparator />
109
+
110
+ {/* 文本对齐 & 列表 */}
111
+ <TextAlignToolbarButton />
112
+ <ListToolbarButton />
113
+ <TiptapEditorToolbarSeparator />
114
+
115
+ {/* 文本格式化: 加粗、删除线、斜体、下划线、颜色 */}
116
+ <MarkToolbarButton format="bold" />
117
+ <MarkToolbarButton format="strike" />
118
+ <MarkToolbarButton format="italic" />
119
+ <MarkToolbarButton format="underline" />
120
+ <ColorHighlightToolbarButton />
121
+ <TiptapEditorToolbarSeparator />
122
+
123
+ {/* 块级元素: 引用、分割线、代码块 */}
124
+ <BlockquoteToolbarButton />
125
+ <HorizontalRuleToolbarButton />
126
+ <CodeBlockToolbarButton />
127
+
128
+ {/* 链接 & 媒体: 图片、附件 */}
129
+ <LinkToolbarButton />
130
+ <ImageUploadToolbarButton />
131
+ <AttachmentToolbarButton />
132
+ </TiptapEditorToolbar>
133
+ );
134
+ }
135
+ ```
136
+
137
+ ### 核心组件
138
+
139
+ | 组件 | 描述 |
140
+ | :----------------------------- | :--------------------------------------- |
141
+ | `TiptapEditor` | 根组件,提供 Editor Context 和编辑器实例 |
142
+ | `TiptapEditorToolbar` | 工具栏容器,用于放置工具栏按钮 |
143
+ | `TiptapEditorContent` | 编辑区域,渲染可编辑内容 |
144
+ | `TiptapEditorToolbarSeparator` | 工具栏分隔符,用于视觉分组 |
145
+
146
+ ### `TiptapEditor` Props
147
+
148
+ | Prop | 类型 | 默认值 | 描述 |
149
+ | :-------------- | :------------------------ | :-------------- | :--------------------- |
150
+ | `value` | `string` | - | 受控值 (HTML) |
151
+ | `defaultValue` | `string` | - | 非受控默认值 |
152
+ | `onValueChange` | `(value: string) => void` | - | 内容变更回调 |
153
+ | `extensions` | `Extensions` | `[CompleteKit]` | 自定义 Tiptap 扩展 |
154
+ | `readOnly` | `boolean` | `false` | 只读模式 |
155
+ | `autoFocus` | `boolean` | - | 自动聚焦 |
156
+ | `asChild` | `boolean` | `false` | 使用子元素作为渲染容器 |
157
+
158
+ ---
159
+
160
+ ## 工具栏按钮参考
161
+
162
+ 所有工具栏按钮均已在 `index.ts` 导出,可直接组合使用:
163
+
164
+ | 组件 | 功能 | Props / 说明 |
165
+ | :---------------------------- | :------------- | :---------------------------------------------------------------- |
166
+ | `UndoRedoToolbarButton` | 撤销 / 重做 | `action: "undo" \| "redo"` |
167
+ | `HeadingToolbarButton` | 标题级别选择 | 下拉菜单,支持正文和 H1-H6 |
168
+ | `MarkToolbarButton` | 文本格式 | `format: "bold" \| "italic" \| "underline" \| "strike" \| "code"` |
169
+ | `ListToolbarButton` | 列表 | 下拉菜单,支持无序、有序、任务列表 |
170
+ | `TextAlignToolbarButton` | 文本对齐 | 下拉菜单,支持左、中、右、两端对齐 |
171
+ | `ColorHighlightToolbarButton` | 文字颜色与高亮 | 颜色选择器 |
172
+ | `LinkToolbarButton` | 超链接 | 点击弹出链接编辑表单 |
173
+ | `BlockquoteToolbarButton` | 引用块 | 切换引用样式 |
174
+ | `CodeBlockToolbarButton` | 代码块 | 插入代码块(Shiki 语法高亮) |
175
+ | `HorizontalRuleToolbarButton` | 水平分割线 | 插入分割线 |
176
+ | `ImageUploadToolbarButton` | 图片上传 | 需配置 `image.upload` 函数 |
177
+ | `AttachmentToolbarButton` | 附件上传 | 需配置 `attachment.upload` 函数,支持 `accept` prop |
178
+
179
+ ---
180
+
181
+ ## 扩展 (Extensions)
182
+
183
+ ### CompleteKit
184
+
185
+ `CompleteKit` 是预配置的扩展集合,包含所有常用编辑功能。它是 `TiptapEditor` 的默认扩展。
186
+
187
+ ```tsx
188
+ import { TiptapEditor, TiptapEditorContent, CompleteKit } from '@/components/business-ui/tiptap-editor';
189
+
190
+ // 使用默认配置
191
+ <TiptapEditor extensions={[CompleteKit]}>
192
+ <TiptapEditorContent />
193
+ </TiptapEditor>
194
+
195
+ // 自定义配置
196
+ <TiptapEditor
197
+ extensions={[
198
+ CompleteKit.configure({
199
+ // 禁用图片扩展
200
+ image: false,
201
+ // 自定义 placeholder
202
+ placeholder: { placeholder: '开始输入...' },
203
+ // 限制标题级别
204
+ heading: { levels: [1, 2, 3] },
205
+ // 禁用链接
206
+ link: false,
207
+ }),
208
+ ]}
209
+ >
210
+ <TiptapEditorContent />
211
+ </TiptapEditor>
212
+ ```
213
+
214
+ #### CompleteKit 包含的扩展
215
+
216
+ | 类别 | 扩展 |
217
+ | :------- | :---------------------------------------------------------- |
218
+ | **基础** | Document, Paragraph, Text, HardBreak, Dropcursor, Gapcursor |
219
+ | **历史** | Undo, Redo |
220
+ | **标题** | Heading (H1-H6) |
221
+ | **文本** | Bold, Italic, Underline, Strike, Code |
222
+ | **颜色** | TextStyle, Color, Highlight |
223
+ | **对齐** | TextAlign |
224
+ | **列表** | BulletList, OrderedList, ListItem, TaskList, TaskItem |
225
+ | **块级** | Blockquote, HorizontalRule, CodeBlock (Shiki) |
226
+ | **链接** | Link (autolink, openOnClick) |
227
+ | **媒体** | Image, Attachment |
228
+ | **其他** | Placeholder |
229
+
230
+ #### CompleteKit 配置选项
231
+
232
+ ```typescript
233
+ interface CompleteKitOptions {
234
+ // 设置为 false 可禁用对应扩展
235
+ heading?: { levels: number[] } | false;
236
+ link?: { autolink?: boolean; openOnClick?: boolean } | false;
237
+ codeBlock?: false;
238
+ taskList?: Partial<TaskListOptions> | false;
239
+ taskItem?: { nested?: boolean } | false;
240
+ textStyle?: Partial<TextStyleOptions> | false;
241
+ color?: Partial<ColorOptions> | false;
242
+ highlight?: { multicolor?: boolean } | false;
243
+ textAlign?: { types: string[] } | false;
244
+ image?: Partial<ImageOptions> | false;
245
+ attachment?: Partial<AttachmentOptions> | false;
246
+ placeholder?: { placeholder?: string } | false;
247
+ // ... 以及所有 StarterKit 选项
248
+ }
249
+ ```
250
+
251
+ ---
252
+
253
+ ## 目录结构
254
+
255
+ ```text
256
+ tiptap-editor/
257
+ ├── index.ts # 统一导出入口
258
+ ├── tiptap-editor.tsx # 核心组件 (TiptapEditor, TiptapEditorToolbar, TiptapEditorContent)
259
+ ├── tiptap-editor-complete.tsx # 完整封装版 (TiptapEditorComplete)
260
+ ├── components/ # 富文本依赖的组件,其中工具栏按钮以 toolbar-button 结尾
261
+ ├── extensions/ # 自定义 Tiptap 扩展
262
+ │ ├── ...
263
+ │ └── complete-kit.ts # 完整扩展集合
264
+ └── hooks/
265
+ └── use-tiptap-editor.ts # 编辑器实例 Hook
266
+ ```
267
+
268
+ ---
269
+
270
+ ## 开发指南
271
+
272
+ ### 添加新的工具栏按钮
273
+
274
+ 1. 在 `components/` 目录创建新组件:
275
+
276
+ ```tsx
277
+ // components/business-ui/tiptap-editor/components/my-toolbar-button.tsx
278
+ 'use client';
279
+
280
+ import { useTiptapEditor } from '@/components/business-ui/tiptap-editor/hooks/use-tiptap-editor';
281
+ import { Button } from '@/components/ui/button';
282
+
283
+ export function MyToolbarButton() {
284
+ const { editor } = useTiptapEditor();
285
+
286
+ if (!editor) return null;
287
+
288
+ return (
289
+ <Button
290
+ variant="ghost"
291
+ size="sm"
292
+ className="size-6 px-0"
293
+ disabled={!editor.can().myCommand()}
294
+ onClick={() => editor.chain().focus().myCommand().run()}
295
+ >
296
+ {/* Icon */}
297
+ </Button>
298
+ );
299
+ }
300
+ ```
301
+
302
+ 2. 在 `index.ts` 中导出:
303
+
304
+ ```tsx
305
+ export { MyToolbarButton } from '@/components/business-ui/tiptap-editor/components/my-toolbar-button';
306
+ ```
307
+
308
+ ### 添加自定义扩展
309
+
310
+ 1. 在 `extensions/` 目录创建扩展文件
311
+ 2. 如需集成到 CompleteKit,修改 `extensions/complete-kit.ts`
312
+ 3. 通过 `extensions` prop 传入:
313
+
314
+ ```tsx
315
+ import { MyExtension } from './extensions/my-extension';
316
+
317
+ <TiptapEditor extensions={[CompleteKit, MyExtension]} />;
318
+ ```
319
+
320
+ ---
321
+
322
+ ## 相关资源
323
+
324
+ - [Tiptap LLM 文档](https://tiptap.dev/llms.txt)
@@ -0,0 +1,74 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Paperclip } from 'lucide-react';
5
+ import { toast } from 'sonner';
6
+
7
+ import { useTiptapEditor } from '@/components/business-ui/tiptap-editor/hooks/use-tiptap-editor';
8
+ import { Button } from '@/components/ui/button';
9
+ import {
10
+ Tooltip,
11
+ TooltipContent,
12
+ TooltipProvider,
13
+ TooltipTrigger,
14
+ } from '@/components/ui/tooltip';
15
+
16
+ interface AttachmentToolbarButtonProps {
17
+ accept?: string;
18
+ }
19
+
20
+ export function AttachmentToolbarButton({
21
+ accept,
22
+ }: AttachmentToolbarButtonProps) {
23
+ const { editor } = useTiptapEditor();
24
+ const fileInputRef = React.useRef<HTMLInputElement>(null);
25
+
26
+ if (!editor) return null;
27
+
28
+ const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
29
+ const files = e.target.files;
30
+ if (!files || files.length === 0) return;
31
+
32
+ const ok = editor
33
+ .chain()
34
+ .focus()
35
+ .insertAttachments(Array.from(files))
36
+ .run();
37
+ if (!ok) {
38
+ toast.error('插入附件失败(请确认 attachment 扩展已启用且提供 upload)');
39
+ }
40
+
41
+ if (fileInputRef.current) {
42
+ fileInputRef.current.value = '';
43
+ }
44
+ };
45
+
46
+ return (
47
+ <TooltipProvider>
48
+ <Tooltip delayDuration={700}>
49
+ <TooltipTrigger asChild>
50
+ <Button
51
+ variant="ghost"
52
+ size="sm"
53
+ className="size-6 px-0"
54
+ onClick={() => fileInputRef.current?.click()}
55
+ >
56
+ <Paperclip className="size-4" />
57
+ <span className="sr-only">上传附件</span>
58
+ </Button>
59
+ </TooltipTrigger>
60
+ <TooltipContent>
61
+ <p>上传附件</p>
62
+ </TooltipContent>
63
+ </Tooltip>
64
+ <input
65
+ type="file"
66
+ multiple
67
+ accept={accept}
68
+ ref={fileInputRef}
69
+ className="hidden"
70
+ onChange={handleFileChange}
71
+ />
72
+ </TooltipProvider>
73
+ );
74
+ }
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import { Quote } from 'lucide-react';
4
+
5
+ import { useTiptapEditor } from '@/components/business-ui/tiptap-editor/hooks/use-tiptap-editor';
6
+ import { Toggle } from '@/components/ui/toggle';
7
+ import {
8
+ Tooltip,
9
+ TooltipContent,
10
+ TooltipProvider,
11
+ TooltipTrigger,
12
+ } from '@/components/ui/tooltip';
13
+
14
+ export function BlockquoteToolbarButton() {
15
+ const { editor } = useTiptapEditor();
16
+ if (!editor) return null;
17
+
18
+ return (
19
+ <TooltipProvider>
20
+ <Tooltip delayDuration={700}>
21
+ <TooltipTrigger asChild>
22
+ <Toggle
23
+ size="sm"
24
+ pressed={editor.isActive('blockquote')}
25
+ onPressedChange={() =>
26
+ editor.chain().focus().toggleBlockquote().run()
27
+ }
28
+ disabled={!editor.can().toggleBlockquote()}
29
+ aria-label="引用"
30
+ className="size-6 p-0"
31
+ >
32
+ <Quote className="size-4" />
33
+ </Toggle>
34
+ </TooltipTrigger>
35
+ <TooltipContent>
36
+ <p>引用(⌘+Shift+&gt;)</p>
37
+ </TooltipContent>
38
+ </Tooltip>
39
+ </TooltipProvider>
40
+ );
41
+ }
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import { CodeXml } from 'lucide-react';
4
+
5
+ import { useTiptapEditor } from '@/components/business-ui/tiptap-editor/hooks/use-tiptap-editor';
6
+ import { Toggle } from '@/components/ui/toggle';
7
+ import {
8
+ Tooltip,
9
+ TooltipContent,
10
+ TooltipProvider,
11
+ TooltipTrigger,
12
+ } from '@/components/ui/tooltip';
13
+
14
+ export function CodeBlockToolbarButton() {
15
+ const { editor } = useTiptapEditor();
16
+ if (!editor) return null;
17
+
18
+ return (
19
+ <TooltipProvider>
20
+ <Tooltip delayDuration={700}>
21
+ <TooltipTrigger asChild>
22
+ <Toggle
23
+ size="sm"
24
+ pressed={editor.isActive('codeBlock')}
25
+ onPressedChange={() =>
26
+ editor.chain().focus().toggleCodeBlock().run()
27
+ }
28
+ disabled={!editor.can().toggleCodeBlock()}
29
+ aria-label="代码块"
30
+ className="size-6 p-0"
31
+ >
32
+ <CodeXml className="size-4" />
33
+ </Toggle>
34
+ </TooltipTrigger>
35
+ <TooltipContent>
36
+ <p>代码块</p>
37
+ </TooltipContent>
38
+ </Tooltip>
39
+ </TooltipProvider>
40
+ );
41
+ }
@@ -0,0 +1,141 @@
1
+ 'use client';
2
+
3
+ import * as React from 'react';
4
+ import { Baseline, Check, ChevronDown } from 'lucide-react';
5
+
6
+ import { useTiptapEditor } from '@/components/business-ui/tiptap-editor/hooks/use-tiptap-editor';
7
+ import { cn } from '@/lib/utils';
8
+ import { Button } from '@/components/ui/button';
9
+ import {
10
+ Popover,
11
+ PopoverContent,
12
+ PopoverTrigger,
13
+ } from '@/components/ui/popover';
14
+ import {
15
+ Tooltip,
16
+ TooltipContent,
17
+ TooltipProvider,
18
+ TooltipTrigger,
19
+ } from '@/components/ui/tooltip';
20
+
21
+ // Design system colors matching Figma
22
+ const TEXT_COLORS = [
23
+ { value: 'var(--foreground)', label: '默认', className: 'bg-foreground' },
24
+ {
25
+ value: 'var(--muted-foreground)',
26
+ label: '次要',
27
+ className: 'bg-muted-foreground',
28
+ },
29
+ { value: 'var(--color-red-600)', label: '红色', className: 'bg-red-600' },
30
+ {
31
+ value: 'var(--color-orange-500)',
32
+ label: '橙色',
33
+ className: 'bg-orange-500',
34
+ },
35
+ {
36
+ value: 'var(--color-yellow-500)',
37
+ label: '黄色',
38
+ className: 'bg-yellow-500',
39
+ },
40
+ { value: 'var(--color-green-500)', label: '绿色', className: 'bg-green-500' },
41
+ { value: 'var(--color-blue-600)', label: '蓝色', className: 'bg-blue-600' },
42
+ {
43
+ value: 'var(--color-purple-600)',
44
+ label: '紫色',
45
+ className: 'bg-purple-600',
46
+ },
47
+ ];
48
+
49
+ export function ColorHighlightToolbarButton() {
50
+ const { editor } = useTiptapEditor();
51
+ const [open, setOpen] = React.useState(false);
52
+
53
+ if (!editor) return null;
54
+
55
+ const currentColor =
56
+ editor.getAttributes('textStyle').color || 'var(--foreground)';
57
+
58
+ const disabled = !editor.can().setColor(TEXT_COLORS[0]!.value);
59
+
60
+ const handleColorSelect = (color: string) => {
61
+ if (color === 'var(--foreground)') {
62
+ editor.chain().focus().unsetColor().run();
63
+ } else {
64
+ editor.chain().focus().setColor(color).run();
65
+ }
66
+ };
67
+
68
+ const handleResetColor = () => {
69
+ editor.chain().focus().unsetColor().run();
70
+ };
71
+
72
+ const trigger = (
73
+ <Button
74
+ variant="ghost"
75
+ size="sm"
76
+ className="h-6 gap-0.5 px-2"
77
+ disabled={disabled}
78
+ >
79
+ <Baseline className="size-4" style={{ color: currentColor }} />
80
+ <ChevronDown className="size-3.5 text-muted-foreground" />
81
+ <span className="sr-only">文字颜色</span>
82
+ </Button>
83
+ );
84
+
85
+ return (
86
+ <Popover open={open} onOpenChange={setOpen}>
87
+ <TooltipProvider>
88
+ <Tooltip delayDuration={700}>
89
+ <TooltipTrigger asChild>
90
+ <PopoverTrigger asChild>{trigger}</PopoverTrigger>
91
+ </TooltipTrigger>
92
+ {!open && (
93
+ <TooltipContent>
94
+ <p>文字颜色</p>
95
+ </TooltipContent>
96
+ )}
97
+ </Tooltip>
98
+ </TooltipProvider>
99
+ <PopoverContent className="w-auto p-4" align="start">
100
+ <div className="flex flex-col items-center gap-3">
101
+ {/* Color grid */}
102
+ <div className="flex gap-1">
103
+ {TEXT_COLORS.map((color) => {
104
+ const isSelected = currentColor === color.value;
105
+
106
+ return (
107
+ <button
108
+ key={color.value}
109
+ className={cn(
110
+ 'relative flex size-6 items-center justify-center rounded-md p-0.5 transition-colors',
111
+ 'hover:ring-1 hover:ring-ring/20',
112
+ isSelected && 'ring-1 ring-ring',
113
+ )}
114
+ onClick={() => handleColorSelect(color.value)}
115
+ title={color.label}
116
+ >
117
+ <span
118
+ className={cn('size-5 rounded-[5px]', color.className)}
119
+ />
120
+ {isSelected && (
121
+ <Check className="absolute size-3.5 text-white" />
122
+ )}
123
+ </button>
124
+ );
125
+ })}
126
+ </div>
127
+
128
+ {/* Reset button */}
129
+ <Button
130
+ variant="outline"
131
+ size="sm"
132
+ className="w-full"
133
+ onClick={handleResetColor}
134
+ >
135
+ 恢复默认
136
+ </Button>
137
+ </div>
138
+ </PopoverContent>
139
+ </Popover>
140
+ );
141
+ }