Editors
This commit is contained in:
		
							
								
								
									
										3377
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3377
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										21
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								package.json
									
									
									
									
									
								
							@ -14,6 +14,23 @@
 | 
			
		||||
    "@dnd-kit/sortable": "^10.0.0",
 | 
			
		||||
    "@dnd-kit/utilities": "^3.2.2",
 | 
			
		||||
    "@hookform/resolvers": "^5.1.1",
 | 
			
		||||
    "@lexical/code": "^0.33.0",
 | 
			
		||||
    "@lexical/list": "^0.33.0",
 | 
			
		||||
    "@lexical/markdown": "^0.33.0",
 | 
			
		||||
    "@lexical/react": "^0.33.0",
 | 
			
		||||
    "@lexical/rich-text": "^0.33.0",
 | 
			
		||||
    "@milkdown/core": "^7.15.1",
 | 
			
		||||
    "@milkdown/plugin-block": "^7.15.1",
 | 
			
		||||
    "@milkdown/plugin-clipboard": "^7.15.1",
 | 
			
		||||
    "@milkdown/plugin-cursor": "^7.15.1",
 | 
			
		||||
    "@milkdown/plugin-history": "^7.15.1",
 | 
			
		||||
    "@milkdown/plugin-indent": "^7.15.1",
 | 
			
		||||
    "@milkdown/plugin-listener": "^7.15.1",
 | 
			
		||||
    "@milkdown/plugin-prism": "^7.15.1",
 | 
			
		||||
    "@milkdown/plugin-tooltip": "^7.15.1",
 | 
			
		||||
    "@milkdown/preset-gfm": "^7.15.1",
 | 
			
		||||
    "@milkdown/react": "^7.15.1",
 | 
			
		||||
    "@milkdown/theme-nord": "^7.15.1",
 | 
			
		||||
    "@prisma/client": "^6.11.1",
 | 
			
		||||
    "@radix-ui/react-checkbox": "^1.3.2",
 | 
			
		||||
    "@radix-ui/react-dialog": "^1.1.14",
 | 
			
		||||
@ -26,9 +43,13 @@
 | 
			
		||||
    "@radix-ui/react-slider": "^1.3.5",
 | 
			
		||||
    "@radix-ui/react-slot": "^1.2.3",
 | 
			
		||||
    "@radix-ui/react-switch": "^1.2.5",
 | 
			
		||||
    "@radix-ui/react-tabs": "^1.1.12",
 | 
			
		||||
    "@radix-ui/react-toggle": "^1.1.9",
 | 
			
		||||
    "@radix-ui/react-toggle-group": "^1.1.10",
 | 
			
		||||
    "class-variance-authority": "^0.7.1",
 | 
			
		||||
    "clsx": "^2.1.1",
 | 
			
		||||
    "cmdk": "^1.1.1",
 | 
			
		||||
    "lexical": "^0.33.0",
 | 
			
		||||
    "lucide-react": "^0.525.0",
 | 
			
		||||
    "next": "15.3.5",
 | 
			
		||||
    "next-auth": "^5.0.0-beta.29",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										15
									
								
								src/app/items/commissions/tos/page.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/app/items/commissions/tos/page.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
"use client"
 | 
			
		||||
 | 
			
		||||
import { MilkdownEditor } from "@/components/items/commissions/tos/editors/MilkdownEditor";
 | 
			
		||||
import { MilkdownProvider } from "@milkdown/react";
 | 
			
		||||
 | 
			
		||||
export default function TosPage() {
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <div className="flex gap-4 justify-between pb-8">
 | 
			
		||||
        <h1 className="text-2xl font-bold mb-4">Terms of Service</h1>
 | 
			
		||||
      </div>
 | 
			
		||||
      <MilkdownProvider><MilkdownEditor /></MilkdownProvider>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -17,6 +17,11 @@ export default function TopNav() {
 | 
			
		||||
            <Link href="/items/commissions/types">CommissionTypes</Link>
 | 
			
		||||
          </NavigationMenuLink>
 | 
			
		||||
        </NavigationMenuItem>
 | 
			
		||||
        <NavigationMenuItem>
 | 
			
		||||
          <NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
 | 
			
		||||
            <Link href="/items/commissions/tos">ToS</Link>
 | 
			
		||||
          </NavigationMenuLink>
 | 
			
		||||
        </NavigationMenuItem>
 | 
			
		||||
      </NavigationMenuList>
 | 
			
		||||
    </NavigationMenu>
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										44
									
								
								src/components/items/commissions/tos/TosEditor.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/components/items/commissions/tos/TosEditor.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,44 @@
 | 
			
		||||
"use client"
 | 
			
		||||
 | 
			
		||||
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
import { EditorContent, useEditor } from '@tiptap/react'
 | 
			
		||||
import StarterKit from '@tiptap/starter-kit'
 | 
			
		||||
import { Bold, Italic, Strikethrough } from "lucide-react"
 | 
			
		||||
 | 
			
		||||
export default function TosEditor() {
 | 
			
		||||
  const editor = useEditor({
 | 
			
		||||
    extensions: [StarterKit],
 | 
			
		||||
    editorProps: {
 | 
			
		||||
      attributes: {
 | 
			
		||||
        class: 'prose prose-sm sm:prose-base lg:prose-lg xl:prose-2xl m-5 focus:outline-none',
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    content: '<p>Hello World! 🌎️</p>',
 | 
			
		||||
    onUpdate: ({ editor }) => {
 | 
			
		||||
      onChange(editor.getHTML())
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  if (!editor) return null
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="space-y-4">
 | 
			
		||||
      <ToggleGroup type="multiple" className="flex gap-1">
 | 
			
		||||
        <ToggleGroupItem onClick={() => editor.chain().focus().toggleBold().run()} pressed={editor.isActive("bold")}>
 | 
			
		||||
          <Bold className="h-4 w-4" />
 | 
			
		||||
        </ToggleGroupItem>
 | 
			
		||||
        <ToggleGroupItem onClick={() => editor.chain().focus().toggleItalic().run()} pressed={editor.isActive("italic")}>
 | 
			
		||||
          <Italic className="h-4 w-4" />
 | 
			
		||||
        </ToggleGroupItem>
 | 
			
		||||
        <ToggleGroupItem onClick={() => editor.chain().focus().toggleStrike().run()} pressed={editor.isActive("strike")}>
 | 
			
		||||
          <Strikethrough className="h-4 w-4" />
 | 
			
		||||
        </ToggleGroupItem>
 | 
			
		||||
      </ToggleGroup>
 | 
			
		||||
 | 
			
		||||
      <div className={cn("border rounded-md p-4 min-h-[200px]", "bg-background text-foreground")}>
 | 
			
		||||
        <EditorContent editor={editor} />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										102
									
								
								src/components/items/commissions/tos/editors/LexicalEditor.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/components/items/commissions/tos/editors/LexicalEditor.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,102 @@
 | 
			
		||||
"use client"
 | 
			
		||||
 | 
			
		||||
import { CodeNode } from "@lexical/code"
 | 
			
		||||
import { LinkNode } from "@lexical/link"
 | 
			
		||||
import { ListItemNode, ListNode } from "@lexical/list"
 | 
			
		||||
import {
 | 
			
		||||
  InitialConfigType,
 | 
			
		||||
  LexicalComposer,
 | 
			
		||||
} from "@lexical/react/LexicalComposer"
 | 
			
		||||
import { ContentEditable } from "@lexical/react/LexicalContentEditable"
 | 
			
		||||
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary"
 | 
			
		||||
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin"
 | 
			
		||||
import { HorizontalRuleNode } from "@lexical/react/LexicalHorizontalRuleNode"
 | 
			
		||||
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin"
 | 
			
		||||
import { ListPlugin } from "@lexical/react/LexicalListPlugin"
 | 
			
		||||
import { MarkdownShortcutPlugin } from "@lexical/react/LexicalMarkdownShortcutPlugin"
 | 
			
		||||
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin"
 | 
			
		||||
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin"
 | 
			
		||||
import { HeadingNode, QuoteNode } from "@lexical/rich-text"
 | 
			
		||||
import { EditorState } from "lexical"
 | 
			
		||||
import { useCallback } from "react"
 | 
			
		||||
import ToolbarPlugin from "./plugins/ToolbarPlugin"
 | 
			
		||||
 | 
			
		||||
type Props = {
 | 
			
		||||
  onChange?: (editorState: EditorState) => void
 | 
			
		||||
  placeholder?: string
 | 
			
		||||
  initialEditorState?: string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default function LexicalEditor({
 | 
			
		||||
  onChange,
 | 
			
		||||
  placeholder = "Write your Terms of Service here...",
 | 
			
		||||
  initialEditorState,
 | 
			
		||||
}: Props) {
 | 
			
		||||
  const initialConfig: InitialConfigType = {
 | 
			
		||||
    namespace: "TosEditor",
 | 
			
		||||
    editorState: initialEditorState ?? undefined,
 | 
			
		||||
    theme: {
 | 
			
		||||
      paragraph: "mb-2",
 | 
			
		||||
      heading: {
 | 
			
		||||
        h1: "text-2xl font-bold",
 | 
			
		||||
        h2: "text-xl font-semibold",
 | 
			
		||||
        h3: "text-lg font-medium",
 | 
			
		||||
      },
 | 
			
		||||
      quote: "border-l-4 pl-4 italic text-muted-foreground",
 | 
			
		||||
      list: {
 | 
			
		||||
        nested: {
 | 
			
		||||
          listitem: "ml-6",
 | 
			
		||||
        },
 | 
			
		||||
        ol: "list-decimal list-inside",
 | 
			
		||||
        ul: "list-disc list-inside",
 | 
			
		||||
        listitem: "my-1",
 | 
			
		||||
      },
 | 
			
		||||
      code: "bg-muted text-sm font-mono rounded p-1",
 | 
			
		||||
    },
 | 
			
		||||
    nodes: [
 | 
			
		||||
      HeadingNode,
 | 
			
		||||
      QuoteNode,
 | 
			
		||||
      CodeNode,
 | 
			
		||||
      ListNode,
 | 
			
		||||
      ListItemNode,
 | 
			
		||||
      HorizontalRuleNode,
 | 
			
		||||
      LinkNode,
 | 
			
		||||
    ],
 | 
			
		||||
    onError: (error) => {
 | 
			
		||||
      console.error("Lexical Error:", error)
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const handleChange = useCallback(
 | 
			
		||||
    (editorState: EditorState) => {
 | 
			
		||||
      if (onChange) onChange(editorState)
 | 
			
		||||
    },
 | 
			
		||||
    [onChange]
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <LexicalComposer initialConfig={initialConfig}>
 | 
			
		||||
      <div className="border rounded-md shadow-sm bg-background text-foreground p-4">
 | 
			
		||||
        <ToolbarPlugin />
 | 
			
		||||
        <div className="relative min-h-[200px] mt-2">
 | 
			
		||||
          <RichTextPlugin
 | 
			
		||||
            contentEditable={
 | 
			
		||||
              <ContentEditable className="min-h-[200px] border rounded-md p-4 bg-background text-foreground focus:outline-none" />
 | 
			
		||||
            }
 | 
			
		||||
            placeholder={
 | 
			
		||||
              <div className="absolute top-2 left-2 text-muted-foreground pointer-events-none text-sm">
 | 
			
		||||
                {placeholder}
 | 
			
		||||
              </div>
 | 
			
		||||
            }
 | 
			
		||||
            ErrorBoundary={LexicalErrorBoundary}
 | 
			
		||||
          />
 | 
			
		||||
        </div>
 | 
			
		||||
        <HistoryPlugin />
 | 
			
		||||
        <ListPlugin />
 | 
			
		||||
        <LinkPlugin />
 | 
			
		||||
        <MarkdownShortcutPlugin />
 | 
			
		||||
        <OnChangePlugin onChange={handleChange} />
 | 
			
		||||
      </div>
 | 
			
		||||
    </LexicalComposer>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,87 @@
 | 
			
		||||
"use client";
 | 
			
		||||
 | 
			
		||||
import { defaultValueCtx, Editor, rootCtx } from "@milkdown/core";
 | 
			
		||||
import { block } from "@milkdown/plugin-block";
 | 
			
		||||
import { clipboard } from "@milkdown/plugin-clipboard";
 | 
			
		||||
import { cursor } from "@milkdown/plugin-cursor";
 | 
			
		||||
import { history } from "@milkdown/plugin-history";
 | 
			
		||||
import { indent } from "@milkdown/plugin-indent";
 | 
			
		||||
import { listener, listenerCtx } from "@milkdown/plugin-listener";
 | 
			
		||||
import { prism } from "@milkdown/plugin-prism";
 | 
			
		||||
import { gfm } from "@milkdown/preset-gfm";
 | 
			
		||||
import { Milkdown, useEditor } from "@milkdown/react";
 | 
			
		||||
import { useState } from "react";
 | 
			
		||||
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
 | 
			
		||||
import { Textarea } from "@/components/ui/textarea";
 | 
			
		||||
 | 
			
		||||
interface MilkdownEditorProps {
 | 
			
		||||
  initialMarkdown?: string;
 | 
			
		||||
  onSave?: (markdown: string, html: string) => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function MilkdownEditor({ initialMarkdown = "", onSave }: MilkdownEditorProps) {
 | 
			
		||||
  const [markdown, setMarkdown] = useState(initialMarkdown);
 | 
			
		||||
  const [html, setHtml] = useState("");
 | 
			
		||||
  const [mode, setMode] = useState<"editor" | "markdown">("editor");
 | 
			
		||||
 | 
			
		||||
  useEditor((root) => {
 | 
			
		||||
    return Editor.make()
 | 
			
		||||
      .config((ctx) => {
 | 
			
		||||
        ctx.set(rootCtx, root);
 | 
			
		||||
        ctx.set(defaultValueCtx, initialMarkdown);
 | 
			
		||||
 | 
			
		||||
        const listener = ctx.get(listenerCtx);
 | 
			
		||||
        listener.updated((_ctx, doc) => {
 | 
			
		||||
          const md = doc?.toString() ?? "";
 | 
			
		||||
          setMarkdown(md);
 | 
			
		||||
 | 
			
		||||
          const htmlContent = document.querySelector(".milkdown")?.innerHTML ?? "";
 | 
			
		||||
          setHtml(htmlContent);
 | 
			
		||||
        });
 | 
			
		||||
      })
 | 
			
		||||
      .use(gfm)
 | 
			
		||||
      .use(listener)
 | 
			
		||||
      .use(history)
 | 
			
		||||
      .use(clipboard)
 | 
			
		||||
      .use(cursor)
 | 
			
		||||
      .use(indent)
 | 
			
		||||
      .use(prism)
 | 
			
		||||
      .use(block);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const handleSave = () => {
 | 
			
		||||
    onSave?.(markdown, html);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="w-full space-y-4">
 | 
			
		||||
      <Tabs value={mode} onValueChange={(value) => setMode(value as "editor" | "markdown")}>
 | 
			
		||||
        <TabsList className="w-full justify-start">
 | 
			
		||||
          <TabsTrigger value="editor">WYSIWYG</TabsTrigger>
 | 
			
		||||
          <TabsTrigger value="markdown">Markdown</TabsTrigger>
 | 
			
		||||
        </TabsList>
 | 
			
		||||
 | 
			
		||||
        <TabsContent value="editor">
 | 
			
		||||
          <div className="border rounded-lg p-4 bg-background">
 | 
			
		||||
            <Milkdown />
 | 
			
		||||
          </div>
 | 
			
		||||
        </TabsContent>
 | 
			
		||||
 | 
			
		||||
        <TabsContent value="markdown">
 | 
			
		||||
          <Textarea
 | 
			
		||||
            value={markdown}
 | 
			
		||||
            onChange={(e) => setMarkdown(e.target.value)}
 | 
			
		||||
            rows={20}
 | 
			
		||||
            className="font-mono"
 | 
			
		||||
          />
 | 
			
		||||
        </TabsContent>
 | 
			
		||||
      </Tabs>
 | 
			
		||||
 | 
			
		||||
      <div className="flex justify-end">
 | 
			
		||||
        <Button onClick={handleSave}>Save</Button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,139 @@
 | 
			
		||||
"use client"
 | 
			
		||||
 | 
			
		||||
import { Button } from "@/components/ui/button"
 | 
			
		||||
import { $createCodeNode } from "@lexical/code"
 | 
			
		||||
import { TOGGLE_LINK_COMMAND } from "@lexical/link"
 | 
			
		||||
import {
 | 
			
		||||
  INSERT_UNORDERED_LIST_COMMAND,
 | 
			
		||||
  REMOVE_LIST_COMMAND,
 | 
			
		||||
} from "@lexical/list"
 | 
			
		||||
import {
 | 
			
		||||
  useLexicalComposerContext,
 | 
			
		||||
} from "@lexical/react/LexicalComposerContext"
 | 
			
		||||
import {
 | 
			
		||||
  $createHeadingNode,
 | 
			
		||||
  $createQuoteNode,
 | 
			
		||||
} from "@lexical/rich-text"
 | 
			
		||||
import {
 | 
			
		||||
  $setBlocksType,
 | 
			
		||||
} from "@lexical/selection"
 | 
			
		||||
import {
 | 
			
		||||
  $createParagraphNode,
 | 
			
		||||
  $getSelection,
 | 
			
		||||
  $isRangeSelection,
 | 
			
		||||
  COMMAND_PRIORITY_CRITICAL,
 | 
			
		||||
  FORMAT_TEXT_COMMAND,
 | 
			
		||||
  SELECTION_CHANGE_COMMAND,
 | 
			
		||||
} from "lexical"
 | 
			
		||||
import { useEffect } from "react"
 | 
			
		||||
 | 
			
		||||
export default function ToolbarPlugin() {
 | 
			
		||||
  const [editor] = useLexicalComposerContext()
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    return editor.registerCommand(
 | 
			
		||||
      SELECTION_CHANGE_COMMAND,
 | 
			
		||||
      () => {
 | 
			
		||||
        const selection = $getSelection()
 | 
			
		||||
        if ($isRangeSelection(selection)) {
 | 
			
		||||
          // You can optionally handle selection changes here.
 | 
			
		||||
        }
 | 
			
		||||
        return false
 | 
			
		||||
      },
 | 
			
		||||
      COMMAND_PRIORITY_CRITICAL
 | 
			
		||||
    )
 | 
			
		||||
  }, [editor])
 | 
			
		||||
 | 
			
		||||
  const format = (formatType: "bold" | "italic" | "underline" | "strikethrough") => {
 | 
			
		||||
    editor.dispatchCommand(FORMAT_TEXT_COMMAND, formatType)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const setHeading = (tag: "h1" | "h2" | "h3" | "paragraph") => {
 | 
			
		||||
    editor.update(() => {
 | 
			
		||||
      const selection = $getSelection()
 | 
			
		||||
      if ($isRangeSelection(selection)) {
 | 
			
		||||
        if (tag === "paragraph") {
 | 
			
		||||
          $setBlocksType(selection, () => $createParagraphNode())
 | 
			
		||||
        } else {
 | 
			
		||||
          $setBlocksType(selection, () => $createHeadingNode(tag))
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const insertQuote = () => {
 | 
			
		||||
    editor.update(() => {
 | 
			
		||||
      const selection = $getSelection()
 | 
			
		||||
      if ($isRangeSelection(selection)) {
 | 
			
		||||
        $setBlocksType(selection, () => $createQuoteNode())
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const insertCodeBlock = () => {
 | 
			
		||||
    editor.update(() => {
 | 
			
		||||
      const selection = $getSelection()
 | 
			
		||||
      if ($isRangeSelection(selection)) {
 | 
			
		||||
        $setBlocksType(selection, () => $createCodeNode())
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const insertBulletList = () => {
 | 
			
		||||
    editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const removeList = () => {
 | 
			
		||||
    editor.dispatchCommand(REMOVE_LIST_COMMAND, undefined)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const toggleLink = () => {
 | 
			
		||||
    editor.dispatchCommand(TOGGLE_LINK_COMMAND, {
 | 
			
		||||
      url: "https://example.com", // optional: replace with modal input
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="flex flex-wrap gap-2 border-b pb-2 mb-2">
 | 
			
		||||
      <Button type="button" variant="outline" size="sm" onClick={() => format("bold")}>
 | 
			
		||||
        Bold
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Button type="button" variant="outline" size="sm" onClick={() => format("italic")}>
 | 
			
		||||
        Italic
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Button type="button" variant="outline" size="sm" onClick={() => format("underline")}>
 | 
			
		||||
        Underline
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Button type="button" variant="outline" size="sm" onClick={() => format("strikethrough")}>
 | 
			
		||||
        Strike
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Button type="button" variant="ghost" size="sm" onClick={() => setHeading("paragraph")}>
 | 
			
		||||
        P
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Button type="button" variant="ghost" size="sm" onClick={() => setHeading("h1")}>
 | 
			
		||||
        H1
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Button type="button" variant="ghost" size="sm" onClick={() => setHeading("h2")}>
 | 
			
		||||
        H2
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Button type="button" variant="ghost" size="sm" onClick={() => setHeading("h3")}>
 | 
			
		||||
        H3
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Button type="button" variant="outline" size="sm" onClick={insertBulletList}>
 | 
			
		||||
        • List
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Button type="button" variant="outline" size="sm" onClick={removeList}>
 | 
			
		||||
        ⛔ List
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Button type="button" variant="outline" size="sm" onClick={insertQuote}>
 | 
			
		||||
        “ Quote
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Button type="button" variant="outline" size="sm" onClick={insertCodeBlock}>
 | 
			
		||||
        Code
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Button type="button" variant="outline" size="sm" onClick={toggleLink}>
 | 
			
		||||
        🔗 Link
 | 
			
		||||
      </Button>
 | 
			
		||||
    </div>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										66
									
								
								src/components/ui/tabs.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								src/components/ui/tabs.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,66 @@
 | 
			
		||||
"use client"
 | 
			
		||||
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
function Tabs({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <TabsPrimitive.Root
 | 
			
		||||
      data-slot="tabs"
 | 
			
		||||
      className={cn("flex flex-col gap-2", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function TabsList({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <TabsPrimitive.List
 | 
			
		||||
      data-slot="tabs-list"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function TabsTrigger({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <TabsPrimitive.Trigger
 | 
			
		||||
      data-slot="tabs-trigger"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function TabsContent({
 | 
			
		||||
  className,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <TabsPrimitive.Content
 | 
			
		||||
      data-slot="tabs-content"
 | 
			
		||||
      className={cn("flex-1 outline-none", className)}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
 | 
			
		||||
							
								
								
									
										18
									
								
								src/components/ui/textarea.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/components/ui/textarea.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
 | 
			
		||||
  return (
 | 
			
		||||
    <textarea
 | 
			
		||||
      data-slot="textarea"
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Textarea }
 | 
			
		||||
							
								
								
									
										73
									
								
								src/components/ui/toggle-group.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								src/components/ui/toggle-group.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,73 @@
 | 
			
		||||
"use client"
 | 
			
		||||
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"
 | 
			
		||||
import { type VariantProps } from "class-variance-authority"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
import { toggleVariants } from "@/components/ui/toggle"
 | 
			
		||||
 | 
			
		||||
const ToggleGroupContext = React.createContext<
 | 
			
		||||
  VariantProps<typeof toggleVariants>
 | 
			
		||||
>({
 | 
			
		||||
  size: "default",
 | 
			
		||||
  variant: "default",
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
function ToggleGroup({
 | 
			
		||||
  className,
 | 
			
		||||
  variant,
 | 
			
		||||
  size,
 | 
			
		||||
  children,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
 | 
			
		||||
  VariantProps<typeof toggleVariants>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <ToggleGroupPrimitive.Root
 | 
			
		||||
      data-slot="toggle-group"
 | 
			
		||||
      data-variant={variant}
 | 
			
		||||
      data-size={size}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        "group/toggle-group flex w-fit items-center rounded-md data-[variant=outline]:shadow-xs",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      <ToggleGroupContext.Provider value={{ variant, size }}>
 | 
			
		||||
        {children}
 | 
			
		||||
      </ToggleGroupContext.Provider>
 | 
			
		||||
    </ToggleGroupPrimitive.Root>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function ToggleGroupItem({
 | 
			
		||||
  className,
 | 
			
		||||
  children,
 | 
			
		||||
  variant,
 | 
			
		||||
  size,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
 | 
			
		||||
  VariantProps<typeof toggleVariants>) {
 | 
			
		||||
  const context = React.useContext(ToggleGroupContext)
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <ToggleGroupPrimitive.Item
 | 
			
		||||
      data-slot="toggle-group-item"
 | 
			
		||||
      data-variant={context.variant || variant}
 | 
			
		||||
      data-size={context.size || size}
 | 
			
		||||
      className={cn(
 | 
			
		||||
        toggleVariants({
 | 
			
		||||
          variant: context.variant || variant,
 | 
			
		||||
          size: context.size || size,
 | 
			
		||||
        }),
 | 
			
		||||
        "min-w-0 flex-1 shrink-0 rounded-none shadow-none first:rounded-l-md last:rounded-r-md focus:z-10 focus-visible:z-10 data-[variant=outline]:border-l-0 data-[variant=outline]:first:border-l",
 | 
			
		||||
        className
 | 
			
		||||
      )}
 | 
			
		||||
      {...props}
 | 
			
		||||
    >
 | 
			
		||||
      {children}
 | 
			
		||||
    </ToggleGroupPrimitive.Item>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { ToggleGroup, ToggleGroupItem }
 | 
			
		||||
							
								
								
									
										47
									
								
								src/components/ui/toggle.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/components/ui/toggle.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,47 @@
 | 
			
		||||
"use client"
 | 
			
		||||
 | 
			
		||||
import * as React from "react"
 | 
			
		||||
import * as TogglePrimitive from "@radix-ui/react-toggle"
 | 
			
		||||
import { cva, type VariantProps } from "class-variance-authority"
 | 
			
		||||
 | 
			
		||||
import { cn } from "@/lib/utils"
 | 
			
		||||
 | 
			
		||||
const toggleVariants = cva(
 | 
			
		||||
  "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] outline-none transition-[color,box-shadow] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive whitespace-nowrap",
 | 
			
		||||
  {
 | 
			
		||||
    variants: {
 | 
			
		||||
      variant: {
 | 
			
		||||
        default: "bg-transparent",
 | 
			
		||||
        outline:
 | 
			
		||||
          "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
 | 
			
		||||
      },
 | 
			
		||||
      size: {
 | 
			
		||||
        default: "h-9 px-2 min-w-9",
 | 
			
		||||
        sm: "h-8 px-1.5 min-w-8",
 | 
			
		||||
        lg: "h-10 px-2.5 min-w-10",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    defaultVariants: {
 | 
			
		||||
      variant: "default",
 | 
			
		||||
      size: "default",
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
function Toggle({
 | 
			
		||||
  className,
 | 
			
		||||
  variant,
 | 
			
		||||
  size,
 | 
			
		||||
  ...props
 | 
			
		||||
}: React.ComponentProps<typeof TogglePrimitive.Root> &
 | 
			
		||||
  VariantProps<typeof toggleVariants>) {
 | 
			
		||||
  return (
 | 
			
		||||
    <TogglePrimitive.Root
 | 
			
		||||
      data-slot="toggle"
 | 
			
		||||
      className={cn(toggleVariants({ variant, size, className }))}
 | 
			
		||||
      {...props}
 | 
			
		||||
    />
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { Toggle, toggleVariants }
 | 
			
		||||
		Reference in New Issue
	
	Block a user