Shadcn Editor

Installation

bash
npx shadcn@latest add zeno-admin/shadcn-editor/editor

也可以使用部署后的 Registry URL:

bash
npx shadcn@latest add https://shadcn-editor-nu.vercel.app/r/editor.json

已有 shadcn/ui 组件的项目在覆盖提示中选择 No,保留项目现有的 UI 实现。

@zenoadmin 收录进 后可使用:

bash
npx shadcn@latest add @zenoadmin/editor

Host locale and theme

Editor 会继承宿主项目根节点的 .dark / .light class 和 shadcn CSS variables,无需单独的 ThemeProvider。 宿主已有语言开关时,使用 <Editor showLanguageSelect={false} /> 隐藏 Editor 内置语言选择器。

ZenoAdmin 可以把 Inertia locale 直接传给 Editor;zh_CN 会自动映射为 zh-CN

tsx
const { locale } = useTranslation();
<EditorI18nProvider  locale={locale}  onLocaleChange={(next) =>    switchAdminLocale(locale, next === "zh-CN" ? "zh_CN" : next)  }>  <Editor /></EditorI18nProvider>;

Usage

ts
import { CodeHighlightNode, CodeNode } from "@lexical/code";import { AutoFocusExtension, ClearEditorExtension, DecoratorTextExtension, HorizontalRuleExtension, SelectionAlwaysOnDisplayExtension } from "@lexical/extension";import { HistoryExtension } from "@lexical/history";import { AutoLinkExtension, ClickableLinkExtension, LinkExtension } from "@lexical/link";import { CheckListExtension, ListExtension } from "@lexical/list";import { CHECK_LIST, ELEMENT_TRANSFORMERS, MULTILINE_ELEMENT_TRANSFORMERS, TEXT_FORMAT_TRANSFORMERS, TEXT_MATCH_TRANSFORMERS } from "@lexical/markdown";import { OverflowNode } from "@lexical/overflow";import { CharacterLimitPlugin } from "@lexical/react/LexicalCharacterLimitPlugin";import { LexicalExtensionComposer } from "@lexical/react/LexicalExtensionComposer";import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";import { TabIndentationPlugin } from "@lexical/react/LexicalTabIndentationPlugin";import { TablePlugin } from "@lexical/react/LexicalTablePlugin";import { RichTextExtension } from "@lexical/rich-text";import { TableCellNode, TableNode, TableRowNode } from "@lexical/table";import { type EditorState, type SerializedEditorState, configExtension, defineExtension } from "lexical";import { useMemo, useState } from "react";
import { useBlockViewer } from "@/components/block-viewer-provider";import { ContentEditable } from "@/components/editor/editor-ui/content-editable";import { DateTimeExtension } from "@/components/editor/extensions/date-time-extension";import { EmojisExtension } from "@/components/editor/extensions/emojis-extension";import { ImagesExtension } from "@/components/editor/extensions/images-extension";import { MarkdownShortcutsExtension } from "@/components/editor/extensions/markdown-shortcuts-extension";import { MaxLengthExtension } from "@/components/editor/extensions/max-length-extension";import { AutocompleteNode } from "@/components/editor/nodes/autocomplete-node";import { TweetNode } from "@/components/editor/nodes/embeds/tweet-node";import { YouTubeNode } from "@/components/editor/nodes/embeds/youtube-node";import { EmojiNode } from "@/components/editor/nodes/emoji-node";import { LayoutContainerNode } from "@/components/editor/nodes/layout-container-node";import { LayoutItemNode } from "@/components/editor/nodes/layout-item-node";import { MentionNode } from "@/components/editor/nodes/mention-node";import { SpecialTextNode } from "@/components/editor/nodes/special-text-node";import { ActionsPlugin } from "@/components/editor/plugins/actions/actions-plugin";import { ClearEditorActionPlugin } from "@/components/editor/plugins/actions/clear-editor-plugin";import { CounterCharacterPlugin } from "@/components/editor/plugins/actions/counter-character-plugin";import { EditModeTogglePlugin } from "@/components/editor/plugins/actions/edit-mode-toggle-plugin";import { ImportExportPlugin } from "@/components/editor/plugins/actions/import-export-plugin";import { MarkdownTogglePlugin } from "@/components/editor/plugins/actions/markdown-toggle-plugin";import { ShareContentPlugin } from "@/components/editor/plugins/actions/share-content-plugin";import { SpeechToTextPlugin } from "@/components/editor/plugins/actions/speech-to-text-plugin";import { TreeViewPlugin } from "@/components/editor/plugins/actions/tree-view-plugin";import { AutoCompletePlugin } from "@/components/editor/plugins/auto-complete-plugin";import { CodeActionMenuPlugin } from "@/components/editor/plugins/code-action-menu-plugin";import { CodeHighlightPlugin } from "@/components/editor/plugins/code-highlight-plugin";import { ComponentPickerMenuPlugin } from "@/components/editor/plugins/component-picker-menu-plugin";import { ContextMenuPlugin } from "@/components/editor/plugins/context-menu-plugin";import { DraggableBlockPlugin } from "@/components/editor/plugins/draggable-block-plugin";import { AutoEmbedPlugin } from "@/components/editor/plugins/embeds/auto-embed-plugin";import { TwitterPlugin } from "@/components/editor/plugins/embeds/twitter-plugin";import { YouTubePlugin } from "@/components/editor/plugins/embeds/youtube-plugin";import { EmojiPickerPlugin } from "@/components/editor/plugins/emoji-picker-plugin";import { FloatingLinkEditorPlugin } from "@/components/editor/plugins/floating-link-editor-plugin";import { FloatingTextFormatToolbarPlugin } from "@/components/editor/plugins/floating-text-format-plugin";import { LayoutPlugin } from "@/components/editor/plugins/layout-plugin";import { MentionsPlugin } from "@/components/editor/plugins/mentions-plugin";import { DateTimePickerPlugin } from "@/components/editor/plugins/picker/date-time-picker-plugin";import { EmbedsPickerPlugin } from "@/components/editor/plugins/picker/embeds-picker-plugin";import { SpecialTextPlugin } from "@/components/editor/plugins/special-text-plugin";import { TabFocusPlugin } from "@/components/editor/plugins/tab-focus-plugin";import { BlockFormatDropDown } from "@/components/editor/plugins/toolbar/block-format-toolbar-plugin";import { FormatBulletedList } from "@/components/editor/plugins/toolbar/block-format/format-bulleted-list";import { FormatCheckList } from "@/components/editor/plugins/toolbar/block-format/format-check-list";import { FormatCodeBlock } from "@/components/editor/plugins/toolbar/block-format/format-code-block";import { FormatHeading } from "@/components/editor/plugins/toolbar/block-format/format-heading";import { FormatNumberedList } from "@/components/editor/plugins/toolbar/block-format/format-numbered-list";import { FormatParagraph } from "@/components/editor/plugins/toolbar/block-format/format-paragraph";import { FormatQuote } from "@/components/editor/plugins/toolbar/block-format/format-quote";import { BlockInsertPlugin } from "@/components/editor/plugins/toolbar/block-insert-plugin";import { InsertColumnsLayout } from "@/components/editor/plugins/toolbar/block-insert/insert-columns-layout";import { InsertEmbeds } from "@/components/editor/plugins/toolbar/block-insert/insert-embeds";import { InsertHorizontalRule } from "@/components/editor/plugins/toolbar/block-insert/insert-horizontal-rule";import { InsertImage } from "@/components/editor/plugins/toolbar/block-insert/insert-image";import { InsertTable } from "@/components/editor/plugins/toolbar/block-insert/insert-table";import { ClearFormattingToolbarPlugin } from "@/components/editor/plugins/toolbar/clear-formatting-toolbar-plugin";import { CodeLanguageToolbarPlugin } from "@/components/editor/plugins/toolbar/code-language-toolbar-plugin";import { ElementFormatToolbarPlugin } from "@/components/editor/plugins/toolbar/element-format-toolbar-plugin";import { FontBackgroundToolbarPlugin } from "@/components/editor/plugins/toolbar/font-background-toolbar-plugin";import { FontColorToolbarPlugin } from "@/components/editor/plugins/toolbar/font-color-toolbar-plugin";import { FontFamilyToolbarPlugin } from "@/components/editor/plugins/toolbar/font-family-toolbar-plugin";import { FontFormatToolbarPlugin } from "@/components/editor/plugins/toolbar/font-format-toolbar-plugin";import { FontSizeToolbarPlugin } from "@/components/editor/plugins/toolbar/font-size-toolbar-plugin";import { HistoryToolbarPlugin } from "@/components/editor/plugins/toolbar/history-toolbar-plugin";import { LinkToolbarPlugin } from "@/components/editor/plugins/toolbar/link-toolbar-plugin";import { SubSuperToolbarPlugin } from "@/components/editor/plugins/toolbar/subsuper-toolbar-plugin";import { ToolbarPlugin } from "@/components/editor/plugins/toolbar/toolbar-plugin";import { editorTheme } from "@/components/editor/themes/editor-theme";import { EMOJI } from "@/components/editor/transformers/markdown-emoji-transformer";import { HR } from "@/components/editor/transformers/markdown-hr-transformer";import { IMAGE } from "@/components/editor/transformers/markdown-image-transformer";import { TABLE } from "@/components/editor/transformers/markdown-table-transformer";import { TWEET } from "@/components/editor/transformers/markdown-tweet-transformer";import { validateUrl } from "@/components/editor/utils/url";import { Separator } from "@/components/ui/separator";import { TooltipProvider } from "@/components/ui/tooltip";
const placeholder = "Press / for commands...";const maxLength = 30;
export function Editor({  editorState,  editorSerializedState,  onChange,  onSerializedChange,}: {  editorState?: EditorState;  editorSerializedState?: SerializedEditorState;  onChange?: (editorState: EditorState) => void;  onSerializedChange?: (editorSerializedState: SerializedEditorState) => void;}) {  const [floatingAnchorElem, setFloatingAnchorElem] =    useState<HTMLDivElement | null>(null);  const [isLinkEditMode, setIsLinkEditMode] = useState<boolean>(false);
  const onRef = (_floatingAnchorElem: HTMLDivElement) => {    if (_floatingAnchorElem !== null) {      setFloatingAnchorElem(_floatingAnchorElem);    }  };
  const AppExtension = useMemo(    () =>      defineExtension({        dependencies: [          RichTextExtension,          AutoFocusExtension,          SelectionAlwaysOnDisplayExtension,          HistoryExtension,          configExtension(LinkExtension, {            validateUrl,            attributes: { rel: "noopener noreferrer", target: "_blank" },          }),          AutoLinkExtension,          ClickableLinkExtension,          configExtension(MaxLengthExtension, { disabled: false, maxLength }),          configExtension(MarkdownShortcutsExtension, {            transformers: [              TABLE, HR, IMAGE, EMOJI, TWEET, CHECK_LIST,              ...ELEMENT_TRANSFORMERS,              ...MULTILINE_ELEMENT_TRANSFORMERS,              ...TEXT_FORMAT_TRANSFORMERS,              ...TEXT_MATCH_TRANSFORMERS,            ],          }),          ClearEditorExtension,          EmojisExtension,          DecoratorTextExtension,          configExtension(ListExtension, { shouldPreserveNumbering: false }),          CheckListExtension,          HorizontalRuleExtension,          ImagesExtension,          DateTimeExtension,        ],        name: "@shadcn-editor",        namespace: "Playground",        nodes: [          OverflowNode,          EmojiNode,          MentionNode,          AutocompleteNode,          SpecialTextNode,          CodeNode,          CodeHighlightNode,          TableNode,          TableCellNode,          TableRowNode,          LayoutContainerNode,          LayoutItemNode,          TweetNode,          YouTubeNode,        ],        $initialEditorState(editor) {          if (editorSerializedState) {            editor.parseEditorState(editorSerializedState);          } else if (editorState) {            editor.setEditorState(editorState);          }        },        theme: editorTheme,      }),    [editorState, editorSerializedState],  );
  return (    <div className="bg-background overflow-hidden rounded-lg border shadow w-full">      <LexicalExtensionComposer extension={AppExtension} contentEditable={null}>        <TooltipProvider>          <div className="relative">            <ToolbarPlugin>              {({ blockType }) => (                <div className="vertical-align-middle sticky top-0 z-10 flex items-center gap-2 overflow-auto border-b p-1">                  <HistoryToolbarPlugin />                  <Separator orientation="vertical" className="h-7!" />                  <BlockFormatDropDown>                    <FormatParagraph />                    <FormatHeading levels={["h1", "h2", "h3"]} />                    <FormatNumberedList />                    <FormatBulletedList />                    <FormatCheckList />                    <FormatCodeBlock />                    <FormatQuote />                  </BlockFormatDropDown>                  {blockType === "code" ? (                    <CodeLanguageToolbarPlugin />                  ) : (                    <>                    <FontFamilyToolbarPlugin />                    <Separator orientation="vertical" className="h-7!" />                    <FontSizeToolbarPlugin />                    <FontFormatToolbarPlugin />                    <SubSuperToolbarPlugin />                    <LinkToolbarPlugin setIsLinkEditMode={setIsLinkEditMode} />                    <ClearFormattingToolbarPlugin />                    <FontColorToolbarPlugin />                    <FontBackgroundToolbarPlugin />                    <ElementFormatToolbarPlugin />                    <BlockInsertPlugin>                      <InsertHorizontalRule />                      <InsertImage />                      <InsertTable />                      <InsertColumnsLayout />                      <InsertEmbeds />                    </BlockInsertPlugin>                    </>                  )}                </div>              )}            </ToolbarPlugin>            <div className="relative">              <div className="">                <div className="" ref={onRef}>                  <ContentEditable                    placeholder={placeholder}                    className="h-[calc(100vh-141px)] pl-4"                  />                </div>              </div>              <ComponentPickerMenuPlugin baseOptions={[]} />              <EmojiPickerPlugin />              <AutoEmbedPlugin />              <MentionsPlugin />              <AutoCompletePlugin />              <ContextMenuPlugin />              <SpecialTextPlugin />              <TabFocusPlugin />              <TabIndentationPlugin />              <CodeHighlightPlugin />              <TablePlugin />              <LayoutPlugin />              <TwitterPlugin />              <YouTubePlugin />              <DraggableBlockPlugin anchorElem={floatingAnchorElem} baseOptions={[]} />              <FloatingTextFormatToolbarPlugin anchorElem={floatingAnchorElem} setIsLinkEditMode={setIsLinkEditMode} />              <FloatingLinkEditorPlugin anchorElem={floatingAnchorElem} isLinkEditMode={isLinkEditMode} setIsLinkEditMode={setIsLinkEditMode} />              <CodeActionMenuPlugin anchorElem={floatingAnchorElem} />            </div>            <ActionsPlugin>              <div className="clear-both flex items-center justify-between gap-2 overflow-auto border-t p-1">                <div className="flex flex-1 justify-start text-xs text-gray-500">                  <CharacterLimitPlugin maxLength={maxLength} charset="UTF-16" />                </div>                <div>                <CounterCharacterPlugin charset="UTF-16" />                </div>                <div className="flex flex-1 justify-end">                  <SpeechToTextPlugin />                  <ShareContentPlugin />                  <ImportExportPlugin />                  <MarkdownTogglePlugin                    shouldPreserveNewLinesInMarkdown={true}                    transformers={[                      TABLE, HR, IMAGE, EMOJI, TWEET, CHECK_LIST,                      ...ELEMENT_TRANSFORMERS,                      ...MULTILINE_ELEMENT_TRANSFORMERS,                      ...TEXT_FORMAT_TRANSFORMERS,                      ...TEXT_MATCH_TRANSFORMERS,                    ]}                  />                  <EditModeTogglePlugin />                  <ClearEditorActionPlugin />                  <TreeViewPlugin />                </div>              </div>            </ActionsPlugin>          </div>
          <OnChangePlugin            ignoreSelectionChange={true}            onChange={(editorState) => {              onChange?.(editorState);              onSerializedChange?.(editorState.toJSON());            }}          />        </TooltipProvider>      </LexicalExtensionComposer>    </div>  );}