本文为开发开源项目的真实开发经历,感兴趣的可以来给我的项目点个star,谢谢啦~
具体博文介绍: 开源|Documind协同文档(接入deepseek-r1、支持实时聊天)Documind 🚀 一个支持实时聊天和接入 - 掘金
前言
由于展示的代码都较为简单,只对个别地方进行讲解,自行阅读或AI辅助阅读即可。
抽离简单组件
这个工具栏由多个工具小组件组成,我们可以将简单的部分抽离成公共组件ToolbarButton
然后通过传入的配置项ToolbarButtonProps
来激活这个公共组件。
typescript
import { cn } from "@/lib/utils";
import { LucideIcon } from "lucide-react";
interface ToolbarButtonProps {
onClick?: () => void; //点击事件
isActive?: boolean; //激活状态样式
icon: LucideIcon; //具体icon
title: string; //名称
}
export const ToolbarButton = ({
onClick,
isActive,
icon: Icon,
title,
}: ToolbarButtonProps) => {
return (
<div className="flex flex-col items-center justify-center">
<button
type="button"
onClick={onClick}
title={title}
className={cn(
"text-sm h-7 min-w-7 flex items-center justify-center rounded-sm hover:bg-neutral-200/80",
isActive && "bg-neutral-200/80"
)}
>
<Icon className="size-4" />
</button>
</div>
);
};
编写配置项
onClick
看不懂的话可以去看官方文档或者问AI
typescript
import {
Undo2Icon,
Redo2Icon,
PrinterIcon,
SpellCheckIcon,
BoldIcon,
ItalicIcon,
UnderlineIcon,
MessageSquareIcon,
ListTodoIcon,
RemoveFormattingIcon,
type LucideIcon,
} from "lucide-react";
import { Editor } from '@tiptap/react';
// 定义section项的类型
export interface ToolbarItem {
label: string;
icon: LucideIcon;
onClick: (editor?: Editor) => void;
isActive?: boolean;
title: string;
}
//传入的editor用于绑定事件
export const getToolbarSections = (editor?: Editor): ToolbarItem[][] => [
[
{
label: "Undo",
icon: Undo2Icon,
onClick: () => editor?.chain().focus().undo().run(),
isActive: false,
title: "Undo",
},
{
label: "Redo",
icon: Redo2Icon,
onClick: () => editor?.chain().focus().redo().run(),
isActive: false,
title: "Redo",
},
{
label: "Print",
icon: PrinterIcon,
onClick: () => {
window.print();
},
title: "Print",
},
{
label: "Spell Check",
icon: SpellCheckIcon,
onClick: () => {
const current = editor?.view.dom.getAttribute("spellcheck");
editor?.view.dom.setAttribute(
"spellcheck",
current === "true" ? "false" : "true"
);
},
title: "Spell Check",
},
],
[
{
label: "Bold",
icon: BoldIcon,
isActive: typeof editor?.isActive === 'function' ? editor.isActive("bold") : false,
onClick: () => editor?.chain().focus().toggleBold().run(),
title: "Bold",
},
{
label: "Italic",
icon: ItalicIcon,
isActive: typeof editor?.isActive === 'function' ? editor.isActive("italic") : false,
onClick: () => editor?.chain().focus().toggleItalic().run(),
title: "Italic",
},
{
label: "Underline",
icon: UnderlineIcon,
isActive: editor?.isActive("underline"),
onClick: () => editor?.chain().focus().toggleUnderline().run(),
title: "Underline",
},
],
[
{
label: "Comment",
icon: MessageSquareIcon,
onClick: () => {
editor?.chain().focus().addPendingComment().run();
},
isActive: editor?.isActive("liveblocksCommentMark"),
title: "Comment",
},
{
label: "List Todo",
icon: ListTodoIcon,
onClick: () => {
editor?.chain().focus().toggleTaskList().run();
},
isActive: editor?.isActive("taskList"),
title: "List Todo",
},
{
label: "Remove Formatting",
icon: RemoveFormattingIcon,
onClick: () => {
editor?.chain().focus().unsetAllMarks().run();
},
title: "Remove Formatting",
},
],
];
组装配置项和公共组件
javascript
import { useEditorStore } from "@/store/use-editor-store";
import { getToolbarSections } from "@/lib/useSections";
const { editor } = useEditorStore();//来自zustand
const sections = getToolbarSections(editor || undefined);//传入的editor用于cnClick事件
{sections[0].map((item) => (
<ToolbarButton key={item.label} {...item} />
))}
{sections[2].map((item) => (
<ToolbarButton key={item.label} {...item} />
))}
编写复杂组件
有些组件功能相对复杂,所以无法抽离成公共组件
FontFamilyButton
组件
使用了shadcn中的DropdownMenu
套件
用于设置字体的fontFamily
typescript
"use client";
import { ChevronDownIcon } from "lucide-react";
import { useEditorStore } from "@/store/use-editor-store";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
//font预设
const fonts = [
{ label: "Arial", value: "Arial" },
{ label: "Times New Roman", value: "Times New Roman" },
{ label: "Courier New", value: "Courier New" },
{ label: "Georgia", value: "Georgia" },
{ label: "Verdana", value: "Verdana" },
];
export const FontFamilyButton = () => {
const { editor } = useEditorStore(); //zustand状态管理
return (
<div className="flex flex-col items-center justify-center">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
title="Select font family"
type="button"
className={cn(
"h-7 w-[120px] shrink-0 flex items-center justify-between rounded-sm hover:bg-neutral-200/80 px-1.5 overflow-hidden text-sm"
)}
>
<span className="truncate">
{editor?.getAttributes("textStyle").fontFamily || "Arial"}
</span>
<ChevronDownIcon className="ml-2 size-4 shrink-0" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-1 flex flex-col gap-y-1">
{fonts.map(({ label, value }) => (
<button
onClick={() => editor?.chain().focus().setFontFamily(value).run()}
key={value}
title="Select font family"
type="button"
className={cn(
"w-full flex items-center gap-x-2 px-2 py-1 rounded-sm hover:bg-neutral-200/80",
editor?.getAttributes("textStyle").fontFamily === value &&
"bg-neutral-200/80"
)}
style={{ fontFamily: value }}
>
<span className="text-sm">{label}</span>
</button>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};
HeadingLevelButton
组件
使用了shadcn中的DropdownMenu
套件
用于设置标题大小
javascript
"use client";
import { type Level } from "@tiptap/extension-heading";
import { ChevronDownIcon } from "lucide-react";
import { useEditorStore } from "@/store/use-editor-store";
import {
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenu,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
export const HeadingLevelButton = () => {
const { editor } = useEditorStore();
const headings = [
{ label: "Normal text", value: 0, fontSize: "16px" },
{ label: "Heading 1", value: 1, fontSize: "32px" },
{ label: "Heading 2", value: 2, fontSize: "24px" },
{ label: "Heading 3", value: 3, fontSize: "20px" },
{ label: "Heading 4", value: 4, fontSize: "18px" },
{ label: "Heading 5", value: 5, fontSize: "16px" },
];
const getCurrentHeading = () => {
for (let level = 1; level <= 5; level++) {
if (editor?.isActive(`heading`, { level })) {
return `Heading ${level}`;
}
}
return "Normal text";
};
return (
<div className="flex flex-col items-center justify-center">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="h-7 w-[120px] flex shrink-0 items-center justify-center rounded-sm hover:bg-neutral-200/80 overflow-hidden text-sm">
<span className="truncate">{getCurrentHeading()}</span>
<ChevronDownIcon className="ml-2 size-4 shrink-0" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-1 flex flex-col gap-y-1">
{headings.map(({ label, value, fontSize }) => (
<button
key={value}
style={{ fontSize }}
onClick={() => {
if (value === 0) {
editor?.chain().focus().setParagraph().run();
} else {
editor
?.chain()
.focus()
.toggleHeading({ level: value as Level })
.run();
}
}}
className={cn(
"flex item-ccenter gap-x-2 px-2 py-1 rounded-sm hover:bg-neutral-200/80",
(value === 0 && !editor?.isActive("heading")) ||
(editor?.isActive("heading", { level: value }) &&
"bg-neutral-200/80")
)}
>
{label}
</button>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};
FontSizeButton
组件
typescript
import { MinusIcon, PlusIcon } from "lucide-react";
import { useEditorStore } from "@/store/use-editor-store";
import { useState, useEffect } from "react";
export const FontSizeButton = () => {
const { editor } = useEditorStore();
// 获取当前字体大小(去除px单位)
const currentFontSize = editor?.getAttributes("textStyle").fontSize
? editor?.getAttributes("textStyle").fontSize.replace("px", "")
: "16";
const [fontSize, setFontSize] = useState(currentFontSize);
const [inputValue, setInputValue] = useState(currentFontSize);
const [isEditing, setIsEditing] = useState(false);
const updateFontSize = (newSize: string) => {
const size = parseInt(newSize); // 将字符串转换为数字
if (!isNaN(size) && size > 0) {
//应用层更新
editor?.chain().focus().setFontSize(`${size}px`).run();
//UI层状态更新
setFontSize(newSize);
setInputValue(newSize);
setIsEditing(false);
}
};
//用于显示当前选中文本的字体大小
useEffect(() => {
const update = () => {
const current = editor?.getAttributes("textStyle").fontSize || "16px";
const newFontSize = current.replace("px", "");
setFontSize(newFontSize);
setInputValue(newFontSize);
setIsEditing(false);
};
//订阅tiptap的selectionUpdate事件
editor?.on("selectionUpdate", update);
// 返回一个清理函数,用于在组件卸载时取消订阅
return () => {
editor?.off("selectionUpdate", update);
};
}, [editor]);
// 在输入框输入内容时,更新输入框的值
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(e.target.value);
};
// 在输入框失去焦点时,更新字体大小
const handleInputBlur = () => {
updateFontSize(inputValue);
};
// 在输入框按下回车键时,更新字体大小
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
e.preventDefault();
updateFontSize(inputValue);
editor?.commands.focus();
}
};
//字号减
const increment = () => {
const newSize = parseInt(fontSize) + 1;
updateFontSize(newSize.toString());
};
//字号加
const decrement = () => {
const newSize = parseInt(fontSize) - 1;
updateFontSize(newSize.toString());
};
return (
<div className="flex items-center gap-x-0.5">
{/* 减号按钮 */}
<button
className="shrink-0 h-7 w-7 flex flex-col items-center justify-center rounded-sm hover:bg-neutral-200/80"
onClick={decrement}
title="font size"
type="button"
>
<MinusIcon className="size-4" />
</button>
{/* 输入框 */}
{isEditing ? (
<input
type="text"
value={inputValue}
onChange={handleInputChange} //编辑中保存
onBlur={handleInputBlur} //失去焦点后保存
onKeyDown={handleKeyDown} //回车保存
className="border border-neutral-400 text-center h-7 w-10 rounded-sm bg-transparent focus:outline-none focus:ring-0"
/>
) : (
<button
className="text-sm border border-neutral-400 text-center h-7 w-10 rounded-sm bg-transparent focus:outline-none focus:ring-0"
onClick={() => {
setIsEditing(true);
setFontSize(currentFontSize);
}}
title="font size"
type="button"
>
{currentFontSize}
</button>
)}
{/* 加号按钮 */}
<button
className="shrink-0 h-7 w-7 flex flex-col items-center justify-center rounded-sm hover:bg-neutral-200/80"
onClick={increment}
title="font size"
type="button"
>
<PlusIcon className="size-4" />
</button>
</div>
);
};
TextColorbutton
组件
使用了shadcn中的DropdownMenu
套件和react-color中的SketchPicker
颜色选择器
用于设置字体颜色
javascript
import { useEditorStore } from "@/store/use-editor-store";
import {
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenu,
} from "@/components/ui/dropdown-menu";
import { type ColorResult, SketchPicker } from "react-color";
export const TextColorbutton = () => {
const { editor } = useEditorStore();
const value = editor?.getAttributes("textStyle").color || "#000000";//当前所选文本颜色
//用于选择颜色后SketchPicker 组件会将其传入onChange
const onChange = (color: ColorResult) => {
editor?.chain().focus().setColor(color.hex).run();
};
return (
<div className="flex flex-col items-center justify-center">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
title="Text Color"
className="text-sm h-7 min-w-7 flex flex-col items-center justify-center rounded-sm hover:bg-neutral-200/80"
>
<span className="text-xs">A</span>
<div
className="h-0.5 w-full"
style={{ backgroundColor: value }}
></div>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0">
<SketchPicker
color={value} // 传入当前颜色以展示
onChange={onChange} //设置tiptap中文本颜色
/>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};
HighlightButton
组件
和上面TextColorbutton
组件相似,这里是用于设置字体的背景颜色
typescript
import { useEditorStore } from "@/store/use-editor-store";
import {
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenu,
} from "@/components/ui/dropdown-menu";
import { type ColorResult, SketchPicker } from "react-color";
import { HighlighterIcon } from "lucide-react";
export const HighlightButton = () => {
const { editor } = useEditorStore();
const value = editor?.getAttributes("highlight").color || "#FFFFFFFF";
const onChange = (color: ColorResult) => {
editor?.chain().focus().setHighlight({ color: color.hex }).run();
};
return (
<div className="flex flex-col items-center justify-center">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
title="Text Color"
className="text-sm h-7 min-w-7 flex flex-col items-center justify-center rounded-sm hover:bg-neutral-200/80"
>
<HighlighterIcon className="size-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0">
<SketchPicker color={value} onChange={onChange} />
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};
LinkButton
组件
使用了shadcn中的DropdownMenu
套件
用于给选中文本添加跳转链接
typescript
import { useEditorStore } from "@/store/use-editor-store";
import { useState } from "react";
import { Link2Icon } from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import {
DropdownMenuContent,
DropdownMenuTrigger,
DropdownMenu,
} from "@/components/ui/dropdown-menu";
export const LinkButton = () => {
const { editor } = useEditorStore();
const [value, setValue] = useState("");
//给选中文本设置链接属性
const onChange = (href: string) => {
editor?.chain().focus().extendMarkRange("link").setLink({ href }).run();
setValue("");
};
return (
<div className="flex flex-col items-center justify-center">
<DropdownMenu
//下拉菜单时提取当前所选文本的链接属性
onOpenChange={(open) => {
if (open) {
setValue(editor?.getAttributes("link").href || "");
}
}}
>
<DropdownMenuTrigger asChild>
<button
title="Text Color"
className="text-sm h-7 min-w-7 flex flex-col items-center justify-center rounded-sm hover:bg-neutral-200/80"
>
<Link2Icon className="size-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-2.5 flex items-center gap-x-2">
<Input
placeholder="https://example.com"
value={value}
onChange={(e) => setValue(e.target.value)}
/>
{/* 点击后触发默认事件关闭下拉菜单 */}
<Button onClick={() => onChange(value)}>Apply</Button>
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};
ImageButton
组件
使用了cloudinary
的上传组件CldUploadButton
依赖下载:npm i next-cloudinary
需要在env环境变量中添加CLOUDINARY_URL=xxxx
(只需在env中设置无需显示调用,在官网获取)uploadPreset
的值也需要从cloudinary
官网获取,具体见next-cloudinary
文档。
此组件引出了闭包捕获问题,具体见文章:
typescript
import { useEditorStore } from "@/store/use-editor-store";
import { ImageIcon } from "lucide-react";
import { CldUploadButton } from "next-cloudinary";
export const ImageButton = () => {
const onChange = (src: string) => {
const currentEditor = useEditorStore.getState().editor;
currentEditor?.chain().focus().setImage({ src }).run();
}
const uploadPhoto = (result: any) => {
onChange(result?.info?.secure_url);
};
return (
<div className="flex flex-col items-center justify-center">
{/* 图片插入下拉菜单 */}
<CldUploadButton
options={{ maxFiles: 1 }}
onSuccess={uploadPhoto}
uploadPreset="官网获取"
>
<ImageIcon className="size-4" />
</CldUploadButton>
</div>
);
};
AlignButton
组件
使用了shadcn中的DropdownMenu
套件
用于提供四种文本对其方式:
- 左对齐(AlignLeft)
- 居中对齐(Align Center)
- 右对齐(AlignRight)
- 两端对齐(AlignJustify)
typescript
import {
AlignCenterIcon,
AlignJustifyIcon,
AlignLeftIcon,
AlignRightIcon,
} from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { useEditorStore } from "@/store/use-editor-store";
export const AlignButton = () => {
const { editor } = useEditorStore();
const alignments = [
{
label: "Align Left",
icon: AlignLeftIcon,
value: "left",
},
{
label: "Align Center",
icon: AlignCenterIcon,
value: "center",
},
{
label: "Align Right",
icon: AlignRightIcon,
value: "right",
},
{
label: "Align Justify",
icon: AlignJustifyIcon,
value: "justify",
},
];
return (
<div className="flex flex-col items-center justify-center">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
title="Align"
className="text-sm h-7 min-w-7 flex flex-col items-center justify-center rounded-sm hover:bg-neutral-200/80"
>
<AlignLeftIcon className="size-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0">
{alignments.map(({ label, icon: Icon, value }) => (
<button
key={value}
title={label}
onClick={() => editor?.chain().focus().setTextAlign(value).run()}
className={cn(
"w-full flex items-center gap-x-2 px-1 py-1 rounded-sm hover:bg-neutral-200/80",
editor?.isActive({ textAlign: value }) && "bg-neutral-200/80"
)}
>
<Icon className="size-4" />
<span className="text-sm">{label}</span>
</button>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};
ListButton
组件
使用了shadcn中的DropdownMenu
套件
用于提供下拉菜单切换无序列表和有序列表
typescript
import { ListIcon, ListOrderedIcon } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { useEditorStore } from "@/store/use-editor-store";
export const ListButton = () => {
const { editor } = useEditorStore();
const lists = [
{
label: "Bullet List",
icon: ListIcon,
isActive: () => editor?.isActive("bulletlist"),
onClick: () => editor?.chain().focus().toggleBulletList().run(),
},
{
label: "Ordered List",
icon: ListOrderedIcon,
isActive: () => editor?.isActive("orderedlist"),
onClick: () => editor?.chain().focus().toggleOrderedList().run(),
},
];
return (
<div className="flex flex-col items-center justify-center">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
title="Align"
className="text-sm h-7 min-w-7 flex flex-col items-center justify-center rounded-sm hover:bg-neutral-200/80"
type="button"
>
<ListIcon className="size-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0">
{lists.map(({ label, icon: Icon, onClick, isActive }) => (
<button
key={label}
onClick={onClick}
className={cn(
"w-full flex items-center gap-x-2 px-1 py-1 rounded-sm hover:bg-neutral-200/80",
isActive() && "bg-neutral-200/80"
)}
>
<Icon className="size-4" />
<span className="text-sm">{label}</span>
</button>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};
LineHeightButton
组件
使用了shadcn中的DropdownMenu
套件
用于切换行高
typescript
import { ListCollapseIcon } from "lucide-react";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
import { useEditorStore } from "@/store/use-editor-store";
export const LineHeightButton = () => {
const { editor } = useEditorStore();
const listHeights = [
{label:"Default",value:"normal"},
{label:"Single",value:"1"},
{label:"1.15",value:"1.15"},
{label:"1.5",value:"1.5"},
{label:"Double",value:"2"},
];
return (
<div className="flex flex-col items-center justify-center">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
title="Align"
className="text-sm h-7 min-w-7 flex flex-col items-center justify-center rounded-sm hover:bg-neutral-200/80"
>
<ListCollapseIcon className="size-4" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent className="p-0">
{listHeights.map(({ label, value }) => (
<button
key={value}
title={label}
onClick={() => editor?.chain().focus().setLineHeight(value).run()}
type="button"
className={cn(
"w-full flex items-center gap-x-2 px-1 py-1 rounded-sm hover:bg-neutral-200/80" ,
editor?.getAttributes("paragraph")?.lineHeight === value && "bg-neutral-200/80"
)}
>
<span className="text-sm">{label}</span>
</button>
))}
</DropdownMenuContent>
</DropdownMenu>
</div>
);
};
拼接成toolbar工具栏组件
Separator
为shadcn中分割线组件
javascript
"use client";
import { ToolbarButton } from "./toolBarButton";
import { useEditorStore } from "@/store/use-editor-store";
import { Separator } from "@/components/ui/separator";
import { FontFamilyButton } from "./fontFamilyButton";
import { getToolbarSections } from "@/lib/useSections";
import { HeadingLevelButton } from "./headingButton";
import { TextColorbutton } from "./textColorButton";
import { HighlightButton } from "./highLightButton";
import { LinkButton } from "./linkButton";
import { ImageButton } from "./imageButton";
import { AlignButton } from "./alignButton";
import { ListButton } from "./ListButton";
import { FontSizeButton } from "./fontSizeButton";
import { LineHeightButton } from "./lineHeightButton";
export const Toolbar = () => {
const { editor } = useEditorStore();
const sections = getToolbarSections(editor || undefined);
return (
<div className="bg-[#F1F4F9] px-2.5 py-0.5 rounded-[24px] min-h-[40px] flex item-center gap-x-0.5 overflow-x-auto ">
{sections[0].map((item) => (
<ToolbarButton key={item.label} {...item} />
))}
{/* 分隔符组件 */}
<div className="flex flex-col items-center justify-center">
<Separator orientation="vertical" className="h-6 bg-neutral-300" />
</div>
<FontFamilyButton />
<div className="flex flex-col items-center justify-center">
<Separator orientation="vertical" className="h-6 bg-neutral-300" />
</div>
<HeadingLevelButton />
<div className="flex flex-col items-center justify-center">
<Separator orientation="vertical" className="h-6 bg-neutral-300" />
</div>
{/* TODO:Font size */}
<FontSizeButton />
<div className="flex flex-col items-center justify-center">
<Separator orientation="vertical" className="h-6 bg-neutral-300" />
</div>
{sections[1].map((item) => (
<ToolbarButton key={item.label} {...item} />
))}
<TextColorbutton />
<HighlightButton />
<div className="flex flex-col items-center justify-center">
<Separator orientation="vertical" className="h-6 bg-neutral-300" />
</div>
<LinkButton />
<ImageButton />
<AlignButton />
<ListButton />
<LineHeightButton />
{sections[2].map((item) => (
<ToolbarButton key={item.label} {...item} />
))}
</div>
);
};