返回 presentation-ai
doc-fixed-toolbar.tsx
根目录 / src / components / plate / ui / doc-fixed-toolbar.tsx
1 "use client";
2
3 import "platejs";
4
5 import {
6 ArrowUpToLineIcon,
7 BaselineIcon,
8 BoldIcon,
9 Code2Icon,
10 ItalicIcon,
11 PaintBucketIcon,
12 StrikethroughIcon,
13 UnderlineIcon,
14 WandSparklesIcon,
15 } from "lucide-react";
16 import { KEYS } from "platejs";
17 import { useEditorReadOnly } from "platejs/react";
18 import { useState } from "react";
19
20 import { cn } from "@/lib/utils";
21 import { useNotesState } from "@/states/notes-state";
22 import { AIToolbarButton } from "./ai-toolbar-button";
23 import { AlignToolbarButton } from "./align-toolbar-button";
24 import { CommentToolbarButton } from "./comment-toolbar-button";
25 import { EmojiToolbarButton } from "./emoji-toolbar-button";
26 import { ExportToolbarButton } from "./export-toolbar-button";
27 import { FontColorToolbarButton } from "./font-color-toolbar-button";
28 import { FontSizeToolbarButton } from "./font-size-toolbar-button";
29 import { RedoToolbarButton, UndoToolbarButton } from "./history-toolbar-button";
30 import { ImportToolbarButton } from "./import-toolbar-button";
31 import {
32 IndentToolbarButton,
33 OutdentToolbarButton,
34 } from "./indent-toolbar-button";
35 import { InsertToolbarButton } from "./insert-toolbar-button";
36 import { LineHeightToolbarButton } from "./line-height-toolbar-button";
37 import { LinkToolbarButton } from "./link-toolbar-button";
38 import {
39 BulletedListToolbarButton,
40 NumberedListToolbarButton,
41 TodoListToolbarButton,
42 } from "./list-toolbar-button";
43 import { MarkToolbarButton } from "./mark-toolbar-button";
44 import { MediaToolbarButton } from "./media-toolbar-button";
45 import { ModeToolbarButton } from "./mode-toolbar-button";
46 import { MoreToolbarButton } from "./more-toolbar-button";
47 import { PrintToolbarButton } from "./print-toolbar-button";
48 import { SuggestionToolbarButton } from "./suggestion-toolbar-button";
49 import { TableToolbarButton } from "./table-toolbar-button";
50 import { TextToDiagramToolbarButton } from "./text-to-diagram-toolbar-button";
51 import { ToggleToolbarButton } from "./toggle-toolbar-button";
52 import { Toolbar, ToolbarGroup, ToolbarSeparator } from "./toolbar";
53 import { TurnIntoToolbarButton } from "./turn-into-toolbar-button";
54
55 type DocTab = "Home" | "Insert" | "Format" | "View";
56
57 const DOC_TABS: DocTab[] = ["Home", "Insert", "Format", "View"];
58
59 /**
60 * Google Docs-style fixed toolbar for the doc editor.
61 *
62 * **Row 1**: Clickable tab labels (File, Home, Insert, Format, Review, View).
63 * The underlined/active tab controls which icon row is visible.
64 *
65 * **Row 2**: Context-dependent icon row based on the selected tab.
66 */
67 export function DocFixedToolbar() {
68 const readOnly = useEditorReadOnly();
69 const isGenerating = useNotesState((state) => state.isGenerating);
70 const [activeTab, setActiveTab] = useState<DocTab>("Home");
71 const shouldHideToolbarActions = readOnly && !isGenerating;
72 const isToolbarInteractionDisabled = isGenerating;
73
74 return (
75 <div
76 className={cn(
77 "fixed-toolbar sticky top-0 left-0 z-50 flex w-full flex-col border-b border-border",
78 "bg-background/95 backdrop-blur-xs supports-backdrop-blur:bg-background/60",
79 )}
80 >
81 {/* Row 1 — Menu tabs */}
82 <div
83 aria-disabled={isToolbarInteractionDisabled}
84 className={cn(
85 "flex items-center gap-0.5 border-b border-border/50 px-3",
86 isToolbarInteractionDisabled && "pointer-events-none opacity-50",
87 )}
88 >
89 {DOC_TABS.map((tab) => (
90 <button
91 type="button"
92 key={tab}
93 onClick={() => setActiveTab(tab)}
94 className={cn(
95 "cursor-pointer rounded-t-sm px-3 py-1.5 text-xs font-medium transition-colors",
96 activeTab === tab
97 ? "border-b-2 border-primary text-primary"
98 : "text-muted-foreground hover:bg-accent/50 hover:text-foreground",
99 )}
100 >
101 {tab}
102 </button>
103 ))}
104 </div>
105
106 {/* Row 2 — Tab-specific toolbar icons */}
107 {!shouldHideToolbarActions && (
108 <Toolbar
109 aria-disabled={isToolbarInteractionDisabled}
110 className={cn(
111 "scrollbar-hide flex items-center overflow-x-auto px-2 py-0.5",
112 isToolbarInteractionDisabled && "pointer-events-none opacity-50",
113 )}
114 >
115 {activeTab === "Home" && (
116 <>
117 {/* Undo / Redo */}
118 <ToolbarGroup className="gap-0">
119 <UndoToolbarButton />
120 <RedoToolbarButton />
121 </ToolbarGroup>
122
123 <ToolbarSeparator />
124
125 {/* Text marks */}
126 <ToolbarGroup className="gap-0">
127 <MarkToolbarButton nodeType={KEYS.bold} tooltip="Bold (⌘+B)">
128 <BoldIcon />
129 </MarkToolbarButton>
130 <MarkToolbarButton
131 nodeType={KEYS.italic}
132 tooltip="Italic (⌘+I)"
133 >
134 <ItalicIcon />
135 </MarkToolbarButton>
136 <MarkToolbarButton
137 nodeType={KEYS.underline}
138 tooltip="Underline (⌘+U)"
139 >
140 <UnderlineIcon />
141 </MarkToolbarButton>
142 <MarkToolbarButton
143 nodeType={KEYS.strikethrough}
144 tooltip="Strikethrough (⌘+⇧+M)"
145 >
146 <StrikethroughIcon />
147 </MarkToolbarButton>
148 <MarkToolbarButton nodeType={KEYS.code} tooltip="Code (⌘+E)">
149 <Code2Icon />
150 </MarkToolbarButton>
151 <LinkToolbarButton />
152 </ToolbarGroup>
153
154 <ToolbarSeparator />
155
156 {/* Font color */}
157 <ToolbarGroup className="gap-0">
158 <FontColorToolbarButton
159 nodeType={KEYS.color}
160 tooltip="Text color"
161 >
162 <BaselineIcon />
163 </FontColorToolbarButton>
164 <FontColorToolbarButton
165 nodeType={KEYS.backgroundColor}
166 tooltip="Background color"
167 >
168 <PaintBucketIcon />
169 </FontColorToolbarButton>
170 </ToolbarGroup>
171
172 <ToolbarSeparator />
173
174 {/* Heading / Font size */}
175 <ToolbarGroup className="gap-0">
176 <TurnIntoToolbarButton />
177 <FontSizeToolbarButton />
178 </ToolbarGroup>
179
180 <ToolbarSeparator />
181
182 {/* Alignment */}
183 <ToolbarGroup className="gap-0">
184 <AlignToolbarButton />
185 </ToolbarGroup>
186
187 <ToolbarSeparator />
188
189 {/* Lists */}
190 <ToolbarGroup className="gap-0">
191 <NumberedListToolbarButton />
192 <BulletedListToolbarButton />
193 <TodoListToolbarButton />
194 <ToggleToolbarButton />
195 </ToolbarGroup>
196
197 <ToolbarSeparator />
198
199 {/* Indent */}
200 <ToolbarGroup className="gap-0">
201 <OutdentToolbarButton />
202 <IndentToolbarButton />
203 <LineHeightToolbarButton />
204 </ToolbarGroup>
205
206 <ToolbarSeparator />
207
208 <ToolbarGroup className="gap-0">
209 <CommentToolbarButton />
210 <SuggestionToolbarButton />
211 </ToolbarGroup>
212 </>
213 )}
214
215 {activeTab === "Insert" && (
216 <>
217 <ToolbarGroup className="gap-0">
218 <InsertToolbarButton />
219 </ToolbarGroup>
220
221 <ToolbarSeparator />
222
223 <ToolbarGroup className="gap-0">
224 <TableToolbarButton />
225 <EmojiToolbarButton />
226 </ToolbarGroup>
227
228 <ToolbarSeparator />
229
230 <ToolbarGroup className="gap-0">
231 <MediaToolbarButton nodeType={KEYS.img} />
232 <MediaToolbarButton nodeType={KEYS.video} />
233 <MediaToolbarButton nodeType={KEYS.audio} />
234 <MediaToolbarButton nodeType={KEYS.file} />
235 </ToolbarGroup>
236
237 <ToolbarSeparator />
238
239 <ToolbarGroup className="gap-0">
240 <LinkToolbarButton />
241 </ToolbarGroup>
242 </>
243 )}
244
245 {activeTab === "Format" && (
246 <>
247 <ToolbarGroup className="gap-0">
248 <MarkToolbarButton nodeType={KEYS.bold} tooltip="Bold (⌘+B)">
249 <BoldIcon />
250 </MarkToolbarButton>
251 <MarkToolbarButton
252 nodeType={KEYS.italic}
253 tooltip="Italic (⌘+I)"
254 >
255 <ItalicIcon />
256 </MarkToolbarButton>
257 <MarkToolbarButton
258 nodeType={KEYS.underline}
259 tooltip="Underline (⌘+U)"
260 >
261 <UnderlineIcon />
262 </MarkToolbarButton>
263 <MarkToolbarButton
264 nodeType={KEYS.strikethrough}
265 tooltip="Strikethrough (⌘+⇧+M)"
266 >
267 <StrikethroughIcon />
268 </MarkToolbarButton>
269 <MarkToolbarButton nodeType={KEYS.code} tooltip="Code (⌘+E)">
270 <Code2Icon />
271 </MarkToolbarButton>
272 </ToolbarGroup>
273
274 <ToolbarSeparator />
275
276 <ToolbarGroup className="gap-0">
277 <FontColorToolbarButton
278 nodeType={KEYS.color}
279 tooltip="Text color"
280 >
281 <BaselineIcon />
282 </FontColorToolbarButton>
283 <FontColorToolbarButton
284 nodeType={KEYS.backgroundColor}
285 tooltip="Background color"
286 >
287 <PaintBucketIcon />
288 </FontColorToolbarButton>
289 </ToolbarGroup>
290
291 <ToolbarSeparator />
292
293 <ToolbarGroup className="gap-0">
294 <AlignToolbarButton />
295 <LineHeightToolbarButton />
296 </ToolbarGroup>
297
298 <ToolbarSeparator />
299
300 <ToolbarGroup className="gap-0">
301 <NumberedListToolbarButton />
302 <BulletedListToolbarButton />
303 <TodoListToolbarButton />
304 </ToolbarGroup>
305
306 <ToolbarSeparator />
307
308 <ToolbarGroup className="gap-0">
309 <OutdentToolbarButton />
310 <IndentToolbarButton />
311 </ToolbarGroup>
312 </>
313 )}
314
315 {activeTab === "View" && (
316 <>
317 <ToolbarGroup className="gap-0">
318 <ModeToolbarButton />
319 </ToolbarGroup>
320
321 <ToolbarSeparator />
322
323 <ToolbarGroup className="gap-0">
324 <MoreToolbarButton />
325 </ToolbarGroup>
326 </>
327 )}
328
329 <ToolbarGroup className="gap-0">
330 <AIToolbarButton tooltip="AI commands">
331 <WandSparklesIcon />
332 </AIToolbarButton>
333 <TextToDiagramToolbarButton />
334 </ToolbarGroup>
335
336 <ToolbarSeparator />
337
338 <ToolbarGroup className="gap-0">
339 <ExportToolbarButton>
340 <ArrowUpToLineIcon />
341 </ExportToolbarButton>
342 <ImportToolbarButton />
343 <PrintToolbarButton />
344 </ToolbarGroup>
345 </Toolbar>
346 )}
347 </div>
348 );
349 }
350
350 lines Plain Text