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/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@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",
|
"@prisma/client": "^6.11.1",
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
"@radix-ui/react-dialog": "^1.1.14",
|
"@radix-ui/react-dialog": "^1.1.14",
|
||||||
@ -26,9 +43,13 @@
|
|||||||
"@radix-ui/react-slider": "^1.3.5",
|
"@radix-ui/react-slider": "^1.3.5",
|
||||||
"@radix-ui/react-slot": "^1.2.3",
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
"@radix-ui/react-switch": "^1.2.5",
|
"@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",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
"lexical": "^0.33.0",
|
||||||
"lucide-react": "^0.525.0",
|
"lucide-react": "^0.525.0",
|
||||||
"next": "15.3.5",
|
"next": "15.3.5",
|
||||||
"next-auth": "^5.0.0-beta.29",
|
"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>
|
<Link href="/items/commissions/types">CommissionTypes</Link>
|
||||||
</NavigationMenuLink>
|
</NavigationMenuLink>
|
||||||
</NavigationMenuItem>
|
</NavigationMenuItem>
|
||||||
|
<NavigationMenuItem>
|
||||||
|
<NavigationMenuLink asChild className={navigationMenuTriggerStyle()}>
|
||||||
|
<Link href="/items/commissions/tos">ToS</Link>
|
||||||
|
</NavigationMenuLink>
|
||||||
|
</NavigationMenuItem>
|
||||||
</NavigationMenuList>
|
</NavigationMenuList>
|
||||||
</NavigationMenu>
|
</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