mirror of
https://github.com/mroxso/zelo-news.git
synced 2026-06-08 21:59:19 +02:00
Add Lexical plugins and update editor functionality in ProfessionalBlogPostForm
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -11,9 +11,13 @@
|
||||
"@fontsource-variable/inter": "^5.2.6",
|
||||
"@getalby/sdk": "^5.1.1",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@lexical/code": "^0.36.2",
|
||||
"@lexical/link": "^0.36.2",
|
||||
"@lexical/list": "^0.36.2",
|
||||
"@lexical/markdown": "^0.36.2",
|
||||
"@lexical/react": "^0.36.2",
|
||||
"@lexical/rich-text": "^0.36.2",
|
||||
"@lexical/selection": "^0.36.2",
|
||||
"@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.4",
|
||||
"@nostrify/react": "npm:@jsr/nostrify__react@^0.2.8",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
|
||||
@@ -13,9 +13,13 @@
|
||||
"@fontsource-variable/inter": "^5.2.6",
|
||||
"@getalby/sdk": "^5.1.1",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@lexical/code": "^0.36.2",
|
||||
"@lexical/link": "^0.36.2",
|
||||
"@lexical/list": "^0.36.2",
|
||||
"@lexical/markdown": "^0.36.2",
|
||||
"@lexical/react": "^0.36.2",
|
||||
"@lexical/rich-text": "^0.36.2",
|
||||
"@lexical/selection": "^0.36.2",
|
||||
"@nostrify/nostrify": "npm:@jsr/nostrify__nostrify@^0.46.4",
|
||||
"@nostrify/react": "npm:@jsr/nostrify__react@^0.2.8",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
|
||||
@@ -258,7 +258,7 @@ export function ProfessionalBlogPostForm({ editIdentifier }: ProfessionalBlogPos
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">
|
||||
{editIdentifier ? 'Edit Article' : 'Write New Article'}
|
||||
{editIdentifier ? 'Edit Article' : 'New Article'}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{editIdentifier ? 'Update your article' : 'Share your thoughts with the world'}
|
||||
@@ -437,7 +437,7 @@ export function ProfessionalBlogPostForm({ editIdentifier }: ProfessionalBlogPos
|
||||
<CardTitle className="text-lg">Content</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={`${isMobile ? 'min-h-[400px]' : 'min-h-[600px]'}`}>
|
||||
<div >
|
||||
<Editor
|
||||
editorSerializedState={editorState}
|
||||
onSerializedChange={(value) => setEditorState(value)}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { HeadingNode, QuoteNode } from "@lexical/rich-text"
|
||||
import { ListNode, ListItemNode } from "@lexical/list"
|
||||
import { CodeNode, CodeHighlightNode } from "@lexical/code"
|
||||
import { LinkNode, AutoLinkNode } from "@lexical/link"
|
||||
import {
|
||||
Klass,
|
||||
LexicalNode,
|
||||
@@ -8,4 +11,15 @@ import {
|
||||
} from "lexical"
|
||||
|
||||
export const nodes: ReadonlyArray<Klass<LexicalNode> | LexicalNodeReplacement> =
|
||||
[HeadingNode, ParagraphNode, TextNode, QuoteNode]
|
||||
[
|
||||
HeadingNode,
|
||||
ParagraphNode,
|
||||
TextNode,
|
||||
QuoteNode,
|
||||
ListNode,
|
||||
ListItemNode,
|
||||
CodeNode,
|
||||
CodeHighlightNode,
|
||||
LinkNode,
|
||||
AutoLinkNode,
|
||||
]
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { useState } from "react"
|
||||
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary"
|
||||
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"
|
||||
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"
|
||||
import { ListPlugin } from "@lexical/react/LexicalListPlugin"
|
||||
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin"
|
||||
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin"
|
||||
import { TRANSFORMERS } from "@lexical/markdown"
|
||||
|
||||
import { ContentEditable } from "@/components/editor/editor-ui/content-editable"
|
||||
import { ToolbarPlugin } from "@/components/editor/plugins/toolbar/ToolbarPlugin"
|
||||
|
||||
export function Plugins() {
|
||||
const [_floatingAnchorElem, setFloatingAnchorElem] =
|
||||
@@ -16,21 +22,28 @@ export function Plugins() {
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* toolbar plugins */}
|
||||
{/* Toolbar */}
|
||||
<ToolbarPlugin />
|
||||
|
||||
{/* Core Plugins */}
|
||||
<HistoryPlugin />
|
||||
<ListPlugin />
|
||||
<LinkPlugin />
|
||||
<MarkdownShortcutPlugin transformers={TRANSFORMERS} />
|
||||
|
||||
{/* Editor */}
|
||||
<div className="relative">
|
||||
<RichTextPlugin
|
||||
contentEditable={
|
||||
<div className="">
|
||||
<div className="min-h-[400px]">
|
||||
<div className="" ref={onRef}>
|
||||
<ContentEditable placeholder={"Start typing ..."} />
|
||||
<ContentEditable placeholder={"Start typing your article..."} />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
ErrorBoundary={LexicalErrorBoundary}
|
||||
/>
|
||||
{/* editor plugins */}
|
||||
</div>
|
||||
{/* actions plugins */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
477
src/components/editor/plugins/toolbar/ToolbarPlugin.tsx
Normal file
477
src/components/editor/plugins/toolbar/ToolbarPlugin.tsx
Normal file
@@ -0,0 +1,477 @@
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
$getSelection,
|
||||
$isRangeSelection,
|
||||
FORMAT_TEXT_COMMAND,
|
||||
FORMAT_ELEMENT_COMMAND,
|
||||
REDO_COMMAND,
|
||||
UNDO_COMMAND,
|
||||
$createParagraphNode,
|
||||
} from 'lexical';
|
||||
import { $setBlocksType } from '@lexical/selection';
|
||||
import {
|
||||
INSERT_ORDERED_LIST_COMMAND,
|
||||
INSERT_UNORDERED_LIST_COMMAND,
|
||||
REMOVE_LIST_COMMAND,
|
||||
$isListNode,
|
||||
ListNode,
|
||||
} from '@lexical/list';
|
||||
import {
|
||||
$createHeadingNode,
|
||||
$createQuoteNode,
|
||||
$isHeadingNode,
|
||||
type HeadingTagType,
|
||||
} from '@lexical/rich-text';
|
||||
import { $isLinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
|
||||
import { $isCodeNode, $createCodeNode } from '@lexical/code';
|
||||
import { $getNearestNodeOfType, mergeRegister } from '@lexical/utils';
|
||||
import {
|
||||
Bold,
|
||||
Italic,
|
||||
Underline,
|
||||
Strikethrough,
|
||||
Code,
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
List,
|
||||
ListOrdered,
|
||||
Quote,
|
||||
Link as LinkIcon,
|
||||
AlignLeft,
|
||||
AlignCenter,
|
||||
AlignRight,
|
||||
AlignJustify,
|
||||
Undo,
|
||||
Redo,
|
||||
FileCode,
|
||||
} from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import { Toggle } from '@/components/ui/toggle';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@/components/ui/popover';
|
||||
|
||||
const LowPriority = 1;
|
||||
|
||||
type BlockType = 'paragraph' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'quote' | 'code' | 'ul' | 'ol';
|
||||
|
||||
export function ToolbarPlugin() {
|
||||
const [editor] = useLexicalComposerContext();
|
||||
const [blockType, setBlockType] = useState<BlockType>('paragraph');
|
||||
const [isBold, setIsBold] = useState(false);
|
||||
const [isItalic, setIsItalic] = useState(false);
|
||||
const [isUnderline, setIsUnderline] = useState(false);
|
||||
const [isStrikethrough, setIsStrikethrough] = useState(false);
|
||||
const [isCode, setIsCode] = useState(false);
|
||||
const [isLink, setIsLink] = useState(false);
|
||||
const [canUndo, setCanUndo] = useState(false);
|
||||
const [canRedo, setCanRedo] = useState(false);
|
||||
const [linkUrl, setLinkUrl] = useState('');
|
||||
const [isLinkPopoverOpen, setIsLinkPopoverOpen] = useState(false);
|
||||
|
||||
const updateToolbar = useCallback(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
// Update text format
|
||||
setIsBold(selection.hasFormat('bold'));
|
||||
setIsItalic(selection.hasFormat('italic'));
|
||||
setIsUnderline(selection.hasFormat('underline'));
|
||||
setIsStrikethrough(selection.hasFormat('strikethrough'));
|
||||
setIsCode(selection.hasFormat('code'));
|
||||
|
||||
// Update link
|
||||
const node = selection.anchor.getNode();
|
||||
const parent = node.getParent();
|
||||
if ($isLinkNode(parent) || $isLinkNode(node)) {
|
||||
setIsLink(true);
|
||||
} else {
|
||||
setIsLink(false);
|
||||
}
|
||||
|
||||
// Update block type
|
||||
const anchorNode = selection.anchor.getNode();
|
||||
const element =
|
||||
anchorNode.getKey() === 'root'
|
||||
? anchorNode
|
||||
: anchorNode.getTopLevelElementOrThrow();
|
||||
|
||||
const elementKey = element.getKey();
|
||||
const elementDOM = editor.getElementByKey(elementKey);
|
||||
|
||||
if (elementDOM !== null) {
|
||||
if ($isListNode(element)) {
|
||||
const parentList = $getNearestNodeOfType(anchorNode, ListNode);
|
||||
const type = parentList ? parentList.getListType() : element.getListType();
|
||||
setBlockType(type === 'number' ? 'ol' : 'ul');
|
||||
} else {
|
||||
const type = $isHeadingNode(element)
|
||||
? element.getTag()
|
||||
: element.getType();
|
||||
setBlockType(type as BlockType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [editor]);
|
||||
|
||||
useEffect(() => {
|
||||
return mergeRegister(
|
||||
editor.registerUpdateListener(({ editorState }) => {
|
||||
editorState.read(() => {
|
||||
updateToolbar();
|
||||
});
|
||||
}),
|
||||
editor.registerCommand(
|
||||
REDO_COMMAND,
|
||||
() => {
|
||||
setCanRedo(editor.getEditorState()._nodeMap.size > 0);
|
||||
return false;
|
||||
},
|
||||
LowPriority
|
||||
),
|
||||
editor.registerCommand(
|
||||
UNDO_COMMAND,
|
||||
() => {
|
||||
setCanUndo(editor.getEditorState()._nodeMap.size > 0);
|
||||
return false;
|
||||
},
|
||||
LowPriority
|
||||
)
|
||||
);
|
||||
}, [editor, updateToolbar]);
|
||||
|
||||
const formatText = (format: 'bold' | 'italic' | 'underline' | 'strikethrough' | 'code') => {
|
||||
editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
|
||||
};
|
||||
|
||||
const formatBlockType = (type: BlockType) => {
|
||||
if (type === 'paragraph') {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
$setBlocksType(selection, () => $createParagraphNode());
|
||||
}
|
||||
});
|
||||
} else if (type === 'h1' || type === 'h2' || type === 'h3' || type === 'h4' || type === 'h5' || type === 'h6') {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
$setBlocksType(selection, () => $createHeadingNode(type as HeadingTagType));
|
||||
}
|
||||
});
|
||||
} else if (type === 'quote') {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
$setBlocksType(selection, () => $createQuoteNode());
|
||||
}
|
||||
});
|
||||
} else if (type === 'code') {
|
||||
editor.update(() => {
|
||||
const selection = $getSelection();
|
||||
if ($isRangeSelection(selection)) {
|
||||
if ($isCodeNode(selection.anchor.getNode())) {
|
||||
$setBlocksType(selection, () => $createParagraphNode());
|
||||
} else {
|
||||
$setBlocksType(selection, () => $createCodeNode());
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (type === 'ul') {
|
||||
if (blockType !== 'ul') {
|
||||
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
||||
} else {
|
||||
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
||||
}
|
||||
} else if (type === 'ol') {
|
||||
if (blockType !== 'ol') {
|
||||
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
||||
} else {
|
||||
editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const formatAlignment = (alignment: 'left' | 'center' | 'right' | 'justify') => {
|
||||
editor.dispatchCommand(FORMAT_ELEMENT_COMMAND, alignment);
|
||||
};
|
||||
|
||||
const insertLink = () => {
|
||||
if (!isLink) {
|
||||
setIsLinkPopoverOpen(true);
|
||||
} else {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLinkSubmit = () => {
|
||||
if (linkUrl) {
|
||||
editor.dispatchCommand(TOGGLE_LINK_COMMAND, linkUrl);
|
||||
setLinkUrl('');
|
||||
setIsLinkPopoverOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-wrap items-center gap-1 p-2 border-b bg-muted/30">
|
||||
{/* Undo/Redo */}
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => editor.dispatchCommand(UNDO_COMMAND, undefined)}
|
||||
disabled={!canUndo}
|
||||
className="h-8 w-8 p-0"
|
||||
title="Undo"
|
||||
>
|
||||
<Undo className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => editor.dispatchCommand(REDO_COMMAND, undefined)}
|
||||
disabled={!canRedo}
|
||||
className="h-8 w-8 p-0"
|
||||
title="Redo"
|
||||
>
|
||||
<Redo className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
|
||||
{/* Block Type Selector */}
|
||||
<Select value={blockType} onValueChange={(value) => formatBlockType(value as BlockType)}>
|
||||
<SelectTrigger className="h-8 w-[140px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="paragraph">Paragraph</SelectItem>
|
||||
<SelectItem value="h1">Heading 1</SelectItem>
|
||||
<SelectItem value="h2">Heading 2</SelectItem>
|
||||
<SelectItem value="h3">Heading 3</SelectItem>
|
||||
<SelectItem value="quote">Quote</SelectItem>
|
||||
<SelectItem value="code">Code Block</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
|
||||
{/* Text Formatting */}
|
||||
<div className="flex gap-1">
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={isBold}
|
||||
onPressedChange={() => formatText('bold')}
|
||||
title="Bold"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Bold className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={isItalic}
|
||||
onPressedChange={() => formatText('italic')}
|
||||
title="Italic"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Italic className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={isUnderline}
|
||||
onPressedChange={() => formatText('underline')}
|
||||
title="Underline"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Underline className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={isStrikethrough}
|
||||
onPressedChange={() => formatText('strikethrough')}
|
||||
title="Strikethrough"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Strikethrough className="h-4 w-4" />
|
||||
</Toggle>
|
||||
<Toggle
|
||||
size="sm"
|
||||
pressed={isCode}
|
||||
onPressedChange={() => formatText('code')}
|
||||
title="Inline Code"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Code className="h-4 w-4" />
|
||||
</Toggle>
|
||||
</div>
|
||||
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
|
||||
{/* Quick Headings */}
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant={blockType === 'h1' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => formatBlockType('h1')}
|
||||
title="Heading 1"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Heading1 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={blockType === 'h2' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => formatBlockType('h2')}
|
||||
title="Heading 2"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Heading2 className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={blockType === 'h3' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => formatBlockType('h3')}
|
||||
title="Heading 3"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Heading3 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
|
||||
{/* Lists and Quote */}
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant={blockType === 'ul' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => formatBlockType('ul')}
|
||||
title="Bullet List"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<List className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={blockType === 'ol' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => formatBlockType('ol')}
|
||||
title="Numbered List"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<ListOrdered className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={blockType === 'quote' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => formatBlockType('quote')}
|
||||
title="Quote"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Quote className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant={blockType === 'code' ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => formatBlockType('code')}
|
||||
title="Code Block"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<FileCode className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
|
||||
{/* Link */}
|
||||
<Popover open={isLinkPopoverOpen} onOpenChange={setIsLinkPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant={isLink ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={insertLink}
|
||||
title="Insert Link"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<LinkIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80">
|
||||
<div className="space-y-2">
|
||||
<h4 className="font-medium text-sm">Insert Link</h4>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
placeholder="https://example.com"
|
||||
value={linkUrl}
|
||||
onChange={(e) => setLinkUrl(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleLinkSubmit();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button size="sm" onClick={handleLinkSubmit}>
|
||||
Insert
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Separator orientation="vertical" className="h-8" />
|
||||
|
||||
{/* Text Alignment */}
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => formatAlignment('left')}
|
||||
title="Align Left"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<AlignLeft className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => formatAlignment('center')}
|
||||
title="Align Center"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<AlignCenter className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => formatAlignment('right')}
|
||||
title="Align Right"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<AlignRight className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => formatAlignment('justify')}
|
||||
title="Justify"
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<AlignJustify className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user