| 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 |