diff --git a/.gitignore b/.gitignore index a547bf3..2bd1803 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.env + # Logs logs *.log diff --git a/@/components/plate-ui/avatar.tsx b/@/components/plate-ui/avatar.tsx new file mode 100644 index 0000000..2a33939 --- /dev/null +++ b/@/components/plate-ui/avatar.tsx @@ -0,0 +1,50 @@ +'use client'; + +import * as React from 'react'; +import * as AvatarPrimitive from '@radix-ui/react-avatar'; + +import { cn } from '../../lib/utils'; + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Avatar.displayName = AvatarPrimitive.Root.displayName; + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarImage.displayName = AvatarPrimitive.Image.displayName; + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName; + +export { Avatar, AvatarImage, AvatarFallback }; diff --git a/@/components/plate-ui/blockquote-element.tsx b/@/components/plate-ui/blockquote-element.tsx new file mode 100644 index 0000000..721652b --- /dev/null +++ b/@/components/plate-ui/blockquote-element.tsx @@ -0,0 +1,25 @@ +'use client'; + +import React from 'react'; +import { PlateElement, PlateElementProps } from '@udecode/plate-common'; + +import { cn } from '../../lib/utils'; + +const BlockquoteElement = React.forwardRef< + React.ElementRef, + PlateElementProps +>(({ className, children, ...props }, ref) => { + return ( + +
{children}
+
+ ); +}); +BlockquoteElement.displayName = 'BlockquoteElement'; + +export { BlockquoteElement }; diff --git a/@/components/plate-ui/button.tsx b/@/components/plate-ui/button.tsx new file mode 100644 index 0000000..28de1c7 --- /dev/null +++ b/@/components/plate-ui/button.tsx @@ -0,0 +1,63 @@ +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, VariantProps } from 'class-variance-authority'; + +import { cn } from '../../lib/utils'; + +const buttonVariants = cva( + 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: + 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + outline: + 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', + secondary: + 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline', + inlineLink: 'text-base text-primary underline underline-offset-4', + }, + size: { + default: 'h-10 px-4 py-2', + xs: 'h-8 rounded-md px-3', + sm: 'h-9 rounded-md px-3', + sms: 'h-9 w-9 rounded-md px-0', + lg: 'h-11 rounded-md px-8', + icon: 'h-10 w-10', + none: '', + }, + isMenu: { + true: 'h-auto w-full cursor-pointer justify-start', + }, + }, + defaultVariants: { + variant: 'default', + size: 'default', + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, isMenu, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button'; + return ( + + ); + } +); +Button.displayName = 'Button'; + +export { Button, buttonVariants }; diff --git a/@/components/plate-ui/caption.tsx b/@/components/plate-ui/caption.tsx new file mode 100644 index 0000000..99ed424 --- /dev/null +++ b/@/components/plate-ui/caption.tsx @@ -0,0 +1,52 @@ +import React, { ComponentProps } from 'react'; +import { + Caption as CaptionPrimitive, + CaptionTextarea as CaptionTextareaPrimitive, +} from '@udecode/plate-caption'; +import { cva, VariantProps } from 'class-variance-authority'; + +import { cn } from '../../lib/utils'; + +const captionVariants = cva('max-w-full', { + variants: { + align: { + left: 'mr-auto', + center: 'mx-auto', + right: 'ml-auto', + }, + }, + defaultVariants: { + align: 'center', + }, +}); + +const Caption = React.forwardRef< + React.ElementRef, + ComponentProps & VariantProps +>(({ className, align, ...props }, ref) => ( + +)); +Caption.displayName = 'Caption'; + +const CaptionTextarea = React.forwardRef< + React.ElementRef, + ComponentProps +>(({ className, ...props }, ref) => ( + +)); +CaptionTextarea.displayName = 'CaptionTextarea'; + +export { Caption, CaptionTextarea }; diff --git a/@/components/plate-ui/checkbox.tsx b/@/components/plate-ui/checkbox.tsx new file mode 100644 index 0000000..880eae1 --- /dev/null +++ b/@/components/plate-ui/checkbox.tsx @@ -0,0 +1,30 @@ +'use client'; + +import * as React from 'react'; +import * as CheckboxPrimitive from '@radix-ui/react-checkbox'; + +import { cn } from '../../lib/utils'; +import { Icons } from './icons'; + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; + +export { Checkbox }; diff --git a/@/components/plate-ui/code-block-combobox.tsx b/@/components/plate-ui/code-block-combobox.tsx new file mode 100644 index 0000000..f918f53 --- /dev/null +++ b/@/components/plate-ui/code-block-combobox.tsx @@ -0,0 +1,90 @@ +'use client'; + +import React, { useState } from 'react'; +import { + CODE_BLOCK_LANGUAGES, + CODE_BLOCK_LANGUAGES_POPULAR, + useCodeBlockCombobox, + useCodeBlockComboboxState, +} from '@udecode/plate-code-block'; + +import { cn } from '../../lib/utils'; +import { Icons } from './icons'; + +import { Button } from './button'; +import { + Command, + CommandEmpty, + CommandInput, + CommandItem, + CommandList, +} from './command'; +import { Popover, PopoverContent, PopoverTrigger } from './popover'; + +const languages: { value: string; label: string }[] = [ + { value: 'text', label: 'Plain Text' }, + ...Object.entries({ + ...CODE_BLOCK_LANGUAGES_POPULAR, + ...CODE_BLOCK_LANGUAGES, + }).map(([key, val]) => ({ + value: key, + label: val as string, + })), +]; + +export function CodeBlockCombobox() { + const state = useCodeBlockComboboxState(); + const { commandItemProps } = useCodeBlockCombobox(state); + + const [open, setOpen] = useState(false); + + if (state.readOnly) return null; + + return ( + + + + + + + + No language found. + + + {languages.map((language) => ( + { + commandItemProps.onSelect(_value); + setOpen(false); + }} + > + + {language.label} + + ))} + + + + + ); +} diff --git a/@/components/plate-ui/code-block-element.css b/@/components/plate-ui/code-block-element.css new file mode 100644 index 0000000..3fc7834 --- /dev/null +++ b/@/components/plate-ui/code-block-element.css @@ -0,0 +1,434 @@ +/** + * One Light theme for prism.js + * Based on Atom's One Light theme: https://github.com/atom/atom/tree/master/packages/one-light-syntax + */ + +/** + * One Light colours (accurate as of commit eb064bf on 19 Feb 2021) + * From colors.less + * --mono-1: hsl(230, 8%, 24%); + * --mono-2: hsl(230, 6%, 44%); + * --mono-3: hsl(230, 4%, 64%) + * --hue-1: hsl(198, 99%, 37%); + * --hue-2: hsl(221, 87%, 60%); + * --hue-3: hsl(301, 63%, 40%); + * --hue-4: hsl(119, 34%, 47%); + * --hue-5: hsl(5, 74%, 59%); + * --hue-5-2: hsl(344, 84%, 43%); + * --hue-6: hsl(35, 99%, 36%); + * --hue-6-2: hsl(35, 99%, 40%); + * --syntax-fg: hsl(230, 8%, 24%); + * --syntax-bg: hsl(230, 1%, 98%); + * --syntax-gutter: hsl(230, 1%, 62%); + * --syntax-guide: hsla(230, 8%, 24%, 0.2); + * --syntax-accent: hsl(230, 100%, 66%); + * From syntax-variables.less + * --syntax-selection-color: hsl(230, 1%, 90%); + * --syntax-gutter-background-color-selected: hsl(230, 1%, 90%); + * --syntax-cursor-line: hsla(230, 8%, 24%, 0.05); + */ + +.slate-code_block code[class*='language-'], +.slate-code_block pre[class*='language-'] { + background: hsl(230, 1%, 98%); + color: hsl(230, 8%, 24%); + font-family: 'Fira Code', 'Fira Mono', Menlo, Consolas, 'DejaVu Sans Mono', + monospace; + direction: ltr; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + line-height: 1.5; + -moz-tab-size: 2; + -o-tab-size: 2; + tab-size: 2; + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; +} + +/* Selection */ +.slate-code_block code[class*='language-']::-moz-selection, +.slate-code_block code[class*='language-'] *::-moz-selection, +.slate-code_block pre[class*='language-'] *::-moz-selection { + background: hsl(230, 1%, 90%); + color: inherit; +} + +.slate-code_block code[class*='language-']::selection, +.slate-code_block code[class*='language-'] *::selection, +.slate-code_block pre[class*='language-'] *::selection { + background: hsl(230, 1%, 90%); + color: inherit; +} + +/* Code blocks */ +.slate-code_block pre[class*='language-'] { + padding: 1em; + margin: 0.5em 0; + overflow: auto; + border-radius: 0.3em; +} + +/* Inline code */ +.slate-code_block :not(pre) > code[class*='language-'] { + padding: 0.2em 0.3em; + border-radius: 0.3em; + white-space: normal; +} + +.token.comment, +.token.prolog, +.token.cdata { + color: hsl(230, 4%, 64%); +} + +.token.doctype, +.token.punctuation, +.token.entity { + color: hsl(230, 8%, 24%); +} + +.token.attr-name, +.token.class-name, +.token.boolean, +.token.constant, +.token.number, +.token.atrule { + color: hsl(35, 99%, 36%); +} + +.token.keyword { + color: hsl(301, 63%, 40%); +} + +.token.property, +.token.tag, +.token.symbol, +.token.deleted, +.token.important { + color: hsl(5, 74%, 59%); +} + +.token.selector, +.token.string, +.token.char, +.token.builtin, +.token.inserted, +.token.regex, +.token.attr-value, +.token.attr-value > .token.punctuation { + color: hsl(119, 34%, 47%); +} + +.token.variable, +.token.operator, +.token.function { + color: hsl(221, 87%, 60%); +} + +.token.url { + color: hsl(198, 99%, 37%); +} + +/* HTML overrides */ +.token.attr-value > .token.punctuation.attr-equals, +.token.special-attr > .token.attr-value > .token.value.css { + color: hsl(230, 8%, 24%); +} + +/* CSS overrides */ +.language-css .token.selector { + color: hsl(5, 74%, 59%); +} + +.language-css .token.property { + color: hsl(230, 8%, 24%); +} + +.language-css .token.function, +.language-css .token.url > .token.function { + color: hsl(198, 99%, 37%); +} + +.language-css .token.url > .token.string.url { + color: hsl(119, 34%, 47%); +} + +.language-css .token.important, +.language-css .token.atrule .token.rule { + color: hsl(301, 63%, 40%); +} + +/* JS overrides */ +.language-javascript .token.operator { + color: hsl(301, 63%, 40%); +} + +.language-javascript + .token.template-string + > .token.interpolation + > .token.interpolation-punctuation.punctuation { + color: hsl(344, 84%, 43%); +} + +/* JSON overrides */ +.language-json .token.operator { + color: hsl(230, 8%, 24%); +} + +.language-json .token.null.keyword { + color: hsl(35, 99%, 36%); +} + +/* MD overrides */ +.language-markdown .token.url, +.language-markdown .token.url > .token.operator, +.language-markdown .token.url-reference.url > .token.string { + color: hsl(230, 8%, 24%); +} + +.language-markdown .token.url > .token.content { + color: hsl(221, 87%, 60%); +} + +.language-markdown .token.url > .token.url, +.language-markdown .token.url-reference.url { + color: hsl(198, 99%, 37%); +} + +.language-markdown .token.blockquote.punctuation, +.language-markdown .token.hr.punctuation { + color: hsl(230, 4%, 64%); + font-style: italic; +} + +.language-markdown .token.code-snippet { + color: hsl(119, 34%, 47%); +} + +.language-markdown .token.bold .token.content { + color: hsl(35, 99%, 36%); +} + +.language-markdown .token.italic .token.content { + color: hsl(301, 63%, 40%); +} + +.language-markdown .token.strike .token.content, +.language-markdown .token.strike .token.punctuation, +.language-markdown .token.list.punctuation, +.language-markdown .token.title.important > .token.punctuation { + color: hsl(5, 74%, 59%); +} + +/* General */ +.token.bold { + font-weight: bold; +} + +.token.comment, +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + +.token.namespace { + opacity: 0.8; +} + +/* Plugin overrides */ +/* Selectors should have higher specificity than those in the plugins' default stylesheets */ + +/* Show Invisibles plugin overrides */ +.token.token.tab:not(:empty):before, +.token.token.cr:before, +.token.token.lf:before, +.token.token.space:before { + color: hsla(230, 8%, 24%, 0.2); +} + +/* Toolbar plugin overrides */ +/* Space out all buttons and move them away from the right edge of the code block */ +div.code-toolbar > .toolbar.toolbar > .toolbar-item { + margin-right: 0.4em; +} + +/* Styling the buttons */ +div.code-toolbar > .toolbar.toolbar > .toolbar-item > button, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > a, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > span { + background: hsl(230, 1%, 90%); + color: hsl(230, 6%, 44%); + padding: 0.1em 0.4em; + border-radius: 0.3em; +} + +div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:hover, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > button:focus, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:hover, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > a:focus, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:hover, +div.code-toolbar > .toolbar.toolbar > .toolbar-item > span:focus { + background: hsl(230, 1%, 78%); /* custom: darken(--syntax-bg, 20%) */ + color: hsl(230, 8%, 24%); +} + +/* Line Highlight plugin overrides */ +/* The highlighted line itself */ +.line-highlight.line-highlight { + background: hsla(230, 8%, 24%, 0.05); +} + +/* Default line numbers in Line Highlight plugin */ +.line-highlight.line-highlight:before, +.line-highlight.line-highlight[data-end]:after { + background: hsl(230, 1%, 90%); + color: hsl(230, 8%, 24%); + padding: 0.1em 0.6em; + border-radius: 0.3em; + box-shadow: 0 2px 0 0 rgba(0, 0, 0, 0.2); /* same as Toolbar plugin default */ +} + +/* Hovering over a linkable line number (in the gutter area) */ +/* Requires Line Numbers plugin as well */ +pre[id].linkable-line-numbers.linkable-line-numbers + span.line-numbers-rows + > span:hover:before { + background-color: hsla(230, 8%, 24%, 0.05); +} + +/* Line Numbers and Command Line plugins overrides */ +/* Line separating gutter from coding area */ +.line-numbers.line-numbers .line-numbers-rows, +.command-line .command-line-prompt { + border-right-color: hsla(230, 8%, 24%, 0.2); +} + +/* Stuff in the gutter */ +.line-numbers .line-numbers-rows > span:before, +.command-line .command-line-prompt > span:before { + color: hsl(230, 1%, 62%); +} + +/* Match Braces plugin overrides */ +/* Note: Outline colour is inherited from the braces */ +.rainbow-braces .token.token.punctuation.brace-level-1, +.rainbow-braces .token.token.punctuation.brace-level-5, +.rainbow-braces .token.token.punctuation.brace-level-9 { + color: hsl(5, 74%, 59%); +} + +.rainbow-braces .token.token.punctuation.brace-level-2, +.rainbow-braces .token.token.punctuation.brace-level-6, +.rainbow-braces .token.token.punctuation.brace-level-10 { + color: hsl(119, 34%, 47%); +} + +.rainbow-braces .token.token.punctuation.brace-level-3, +.rainbow-braces .token.token.punctuation.brace-level-7, +.rainbow-braces .token.token.punctuation.brace-level-11 { + color: hsl(221, 87%, 60%); +} + +.rainbow-braces .token.token.punctuation.brace-level-4, +.rainbow-braces .token.token.punctuation.brace-level-8, +.rainbow-braces .token.token.punctuation.brace-level-12 { + color: hsl(301, 63%, 40%); +} + +/* Diff Highlight plugin overrides */ +/* Taken from https://github.com/atom/github/blob/master/styles/variables.less */ +pre.diff-highlight > code .token.token.deleted:not(.prefix), +pre > code.diff-highlight .token.token.deleted:not(.prefix) { + background-color: hsla(353, 100%, 66%, 0.15); +} + +pre.diff-highlight > code .token.token.deleted:not(.prefix)::-moz-selection, +pre.diff-highlight > code .token.token.deleted:not(.prefix) *::-moz-selection, +pre > code.diff-highlight .token.token.deleted:not(.prefix)::-moz-selection, +pre > code.diff-highlight .token.token.deleted:not(.prefix) *::-moz-selection { + background-color: hsla(353, 95%, 66%, 0.25); +} + +pre.diff-highlight > code .token.token.deleted:not(.prefix)::selection, +pre.diff-highlight > code .token.token.deleted:not(.prefix) *::selection, +pre > code.diff-highlight .token.token.deleted:not(.prefix)::selection, +pre > code.diff-highlight .token.token.deleted:not(.prefix) *::selection { + background-color: hsla(353, 95%, 66%, 0.25); +} + +pre.diff-highlight > code .token.token.inserted:not(.prefix), +pre > code.diff-highlight .token.token.inserted:not(.prefix) { + background-color: hsla(137, 100%, 55%, 0.15); +} + +pre.diff-highlight > code .token.token.inserted:not(.prefix)::-moz-selection, +pre.diff-highlight > code .token.token.inserted:not(.prefix) *::-moz-selection, +pre > code.diff-highlight .token.token.inserted:not(.prefix)::-moz-selection, +pre > code.diff-highlight .token.token.inserted:not(.prefix) *::-moz-selection { + background-color: hsla(135, 73%, 55%, 0.25); +} + +pre.diff-highlight > code .token.token.inserted:not(.prefix)::selection, +pre.diff-highlight > code .token.token.inserted:not(.prefix) *::selection, +pre > code.diff-highlight .token.token.inserted:not(.prefix)::selection, +pre > code.diff-highlight .token.token.inserted:not(.prefix) *::selection { + background-color: hsla(135, 73%, 55%, 0.25); +} + +/* Previewers plugin overrides */ +/* Based on https://github.com/atom-community/atom-ide-datatip/blob/master/styles/atom-ide-datatips.less and https://github.com/atom/atom/blob/master/packages/one-light-ui */ +/* Border around popup */ +.prism-previewer.prism-previewer:before, +.prism-previewer-gradient.prism-previewer-gradient div { + border-color: hsl(0, 0, 95%); +} + +/* Angle and time should remain as circles and are hence not included */ +.prism-previewer-color.prism-previewer-color:before, +.prism-previewer-gradient.prism-previewer-gradient div, +.prism-previewer-easing.prism-previewer-easing:before { + border-radius: 0.3em; +} + +/* Triangles pointing to the code */ +.prism-previewer.prism-previewer:after { + border-top-color: hsl(0, 0, 95%); +} + +.prism-previewer-flipped.prism-previewer-flipped.after { + border-bottom-color: hsl(0, 0, 95%); +} + +/* Background colour within the popup */ +.prism-previewer-angle.prism-previewer-angle:before, +.prism-previewer-time.prism-previewer-time:before, +.prism-previewer-easing.prism-previewer-easing { + background: hsl(0, 0%, 100%); +} + +/* For angle, this is the positive area (eg. 90deg will display one quadrant in this colour) */ +/* For time, this is the alternate colour */ +.prism-previewer-angle.prism-previewer-angle circle, +.prism-previewer-time.prism-previewer-time circle { + stroke: hsl(230, 8%, 24%); + stroke-opacity: 1; +} + +/* Stroke colours of the handle, direction point, and vector itself */ +.prism-previewer-easing.prism-previewer-easing circle, +.prism-previewer-easing.prism-previewer-easing path, +.prism-previewer-easing.prism-previewer-easing line { + stroke: hsl(230, 8%, 24%); +} + +/* Fill colour of the handle */ +.prism-previewer-easing.prism-previewer-easing circle { + fill: transparent; +} diff --git a/@/components/plate-ui/code-block-element.tsx b/@/components/plate-ui/code-block-element.tsx new file mode 100644 index 0000000..9c751f3 --- /dev/null +++ b/@/components/plate-ui/code-block-element.tsx @@ -0,0 +1,47 @@ +'use client'; + +import './code-block-element.css'; + +import React, { forwardRef } from 'react'; +import { + TCodeBlockElement, + useCodeBlockElementState, +} from '@udecode/plate-code-block'; +import { PlateElement, PlateElementProps, Value } from '@udecode/plate-common'; + +import { cn } from '../../lib/utils'; + +import { CodeBlockCombobox } from './code-block-combobox'; + +const CodeBlockElement = forwardRef< + HTMLDivElement, + PlateElementProps +>(({ className, ...props }, ref) => { + const { children, element } = props; + + const state = useCodeBlockElementState({ element }); + + return ( + +
+        {children}
+      
+ + {state.syntax && ( +
+ +
+ )} +
+ ); +}); +CodeBlockElement.displayName = 'CodeBlockElement'; + +export { CodeBlockElement }; diff --git a/@/components/plate-ui/code-leaf.tsx b/@/components/plate-ui/code-leaf.tsx new file mode 100644 index 0000000..dc3fdec --- /dev/null +++ b/@/components/plate-ui/code-leaf.tsx @@ -0,0 +1,22 @@ +'use client'; + +import React from 'react'; +import { PlateLeaf, PlateLeafProps } from '@udecode/plate-common'; + +import { cn } from '../../lib/utils'; + +export function CodeLeaf({ className, children, ...props }: PlateLeafProps) { + return ( + + {children} + + ); +} diff --git a/@/components/plate-ui/code-line-element.tsx b/@/components/plate-ui/code-line-element.tsx new file mode 100644 index 0000000..316f450 --- /dev/null +++ b/@/components/plate-ui/code-line-element.tsx @@ -0,0 +1,11 @@ +'use client'; + +import React, { forwardRef } from 'react'; +import { PlateElement, PlateElementProps } from '@udecode/plate-common'; + +const CodeLineElement = forwardRef( + (props, ref) => +); +CodeLineElement.displayName = 'CodeLineElement'; + +export { CodeLineElement }; diff --git a/@/components/plate-ui/code-syntax-leaf.tsx b/@/components/plate-ui/code-syntax-leaf.tsx new file mode 100644 index 0000000..266e234 --- /dev/null +++ b/@/components/plate-ui/code-syntax-leaf.tsx @@ -0,0 +1,17 @@ +'use client'; + +import React from 'react'; +import { useCodeSyntaxLeaf } from '@udecode/plate-code-block'; +import { PlateLeaf, PlateLeafProps } from '@udecode/plate-common'; + +export function CodeSyntaxLeaf({ children, ...props }: PlateLeafProps) { + const { leaf } = props; + + const { tokenProps } = useCodeSyntaxLeaf({ leaf }); + + return ( + + {children} + + ); +} diff --git a/@/components/plate-ui/combobox.tsx b/@/components/plate-ui/combobox.tsx new file mode 100644 index 0000000..4bd1a64 --- /dev/null +++ b/@/components/plate-ui/combobox.tsx @@ -0,0 +1,156 @@ +'use client'; + +import React, { useEffect } from 'react'; +import * as Popover from '@radix-ui/react-popover'; +import { + comboboxActions, + ComboboxContentItemProps, + ComboboxContentProps, + ComboboxProps, + Data, + NoData, + TComboboxItem, + useActiveComboboxStore, + useComboboxContent, + useComboboxContentState, + useComboboxControls, + useComboboxItem, + useComboboxSelectors, +} from '@udecode/plate-combobox'; +import { useEditorState, useEventEditorSelectors } from '@udecode/plate-common'; +import { createVirtualRef } from '@udecode/plate-floating'; + +import { cn } from '../../lib/utils'; + +export function ComboboxItem({ + combobox, + index, + item, + onRenderItem, +}: ComboboxContentItemProps) { + const { props } = useComboboxItem({ item, index, combobox, onRenderItem }); + + return ( +
+ ); +} + +export function ComboboxContent( + props: ComboboxContentProps +) { + const { + component: Component, + items, + portalElement, + combobox, + onRenderItem, + } = props; + + const editor = useEditorState(); + + const filteredItems = + useComboboxSelectors.filteredItems() as TComboboxItem[]; + const activeComboboxStore = useActiveComboboxStore()!; + + const state = useComboboxContentState({ items, combobox }); + const { menuProps, targetRange } = useComboboxContent(state); + + return ( + + + + + event.preventDefault()} + > + {Component ? Component({ store: activeComboboxStore }) : null} + + {filteredItems.map((item, index) => ( + + ))} + + + + ); +} + +/** + * Register the combobox id, trigger, onSelectItem + * Renders the combobox if active. + */ +export function Combobox({ + id, + trigger, + searchPattern, + onSelectItem, + controlled, + maxSuggestions, + filter, + sort, + disabled: _disabled, + ...props +}: ComboboxProps) { + const storeItems = useComboboxSelectors.items(); + const disabled = + _disabled ?? (storeItems.length === 0 && !props.items?.length); + + const focusedEditorId = useEventEditorSelectors.focus?.(); + const combobox = useComboboxControls(); + const activeId = useComboboxSelectors.activeId(); + const editor = useEditorState(); + + useEffect(() => { + comboboxActions.setComboboxById({ + id, + trigger, + searchPattern, + controlled, + onSelectItem, + maxSuggestions, + filter, + sort, + }); + }, [ + id, + trigger, + searchPattern, + controlled, + onSelectItem, + maxSuggestions, + filter, + sort, + ]); + + if ( + !combobox || + !editor.selection || + focusedEditorId !== editor.id || + activeId !== id || + disabled + ) { + return null; + } + + return ; +} diff --git a/@/components/plate-ui/command.tsx b/@/components/plate-ui/command.tsx new file mode 100644 index 0000000..72ae042 --- /dev/null +++ b/@/components/plate-ui/command.tsx @@ -0,0 +1,156 @@ +'use client'; + +import * as React from 'react'; +import { DialogProps } from '@radix-ui/react-dialog'; +import { Command as CommandPrimitive } from 'cmdk'; + +import { cn } from '../../lib/utils'; +import { Icons } from './icons'; + +import { Dialog, DialogContent } from './dialog'; + +const Command = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Command.displayName = CommandPrimitive.displayName; + +interface CommandDialogProps extends DialogProps {} + +function CommandDialog({ children, ...props }: CommandDialogProps) { + return ( + + + + {children} + + + + ); +} + +const CommandInput = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( +
+ + +
+)); + +CommandInput.displayName = CommandPrimitive.Input.displayName; + +const CommandList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandList.displayName = CommandPrimitive.List.displayName; + +const CommandEmpty = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>((props, ref) => ( + +)); + +CommandEmpty.displayName = CommandPrimitive.Empty.displayName; + +const CommandGroup = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandGroup.displayName = CommandPrimitive.Group.displayName; + +const CommandSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +CommandSeparator.displayName = CommandPrimitive.Separator.displayName; + +const CommandItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); + +CommandItem.displayName = CommandPrimitive.Item.displayName; + +function CommandShortcut({ + className, + ...props +}: React.HTMLAttributes) { + return ( + + ); +} +CommandShortcut.displayName = 'CommandShortcut'; + +export { + Command, + CommandDialog, + CommandInput, + CommandList, + CommandEmpty, + CommandGroup, + CommandItem, + CommandShortcut, + CommandSeparator, +}; diff --git a/@/components/plate-ui/comment-avatar.tsx b/@/components/plate-ui/comment-avatar.tsx new file mode 100644 index 0000000..e43665b --- /dev/null +++ b/@/components/plate-ui/comment-avatar.tsx @@ -0,0 +1,22 @@ +'use client'; + +import React from 'react'; +import { useUserById } from '@udecode/plate-comments'; + +import { + Avatar, + AvatarFallback, + AvatarImage, +} from './avatar'; + +export function CommentAvatar({ userId }: { userId: string | null }) { + const user = useUserById(userId); + if (!user) return null; + + return ( + + + {user.name?.[0]} + + ); +} diff --git a/@/components/plate-ui/comment-create-form.tsx b/@/components/plate-ui/comment-create-form.tsx new file mode 100644 index 0000000..5af4380 --- /dev/null +++ b/@/components/plate-ui/comment-create-form.tsx @@ -0,0 +1,34 @@ +'use client'; + +import React from 'react'; +import { + CommentNewSubmitButton, + CommentNewTextarea, + useCommentsSelectors, +} from '@udecode/plate-comments'; + +import { cn } from '../../lib/utils'; +import { buttonVariants } from './button'; +import { inputVariants } from './input'; + +import { CommentAvatar } from './comment-avatar'; + +export function CommentCreateForm() { + const myUserId = useCommentsSelectors().myUserId(); + + return ( +
+ + +
+ + + + Comment + +
+
+ ); +} diff --git a/@/components/plate-ui/comment-item.tsx b/@/components/plate-ui/comment-item.tsx new file mode 100644 index 0000000..c4068ba --- /dev/null +++ b/@/components/plate-ui/comment-item.tsx @@ -0,0 +1,70 @@ +'use client'; + +import React from 'react'; +import { + CommentProvider, + useCommentById, + useCommentItemContentState, +} from '@udecode/plate-comments'; +import { formatDistance } from 'date-fns'; + +import { CommentAvatar } from './comment-avatar'; +import { CommentMoreDropdown } from './comment-more-dropdown'; +import { CommentResolveButton } from './comment-resolve-button'; +import { CommentValue } from './comment-value'; + +type PlateCommentProps = { + commentId: string; +}; + +function CommentItemContent() { + const { + comment, + isMyComment, + isReplyComment, + user, + editingValue, + commentText, + } = useCommentItemContentState(); + + return ( +
+
+ + +

{user?.name}

+ +
+ {formatDistance(comment.createdAt, Date.now())} ago +
+ + {isMyComment && ( +
+ {isReplyComment ? null : } + + +
+ )} +
+ +
+ {editingValue ? ( + + ) : ( +
{commentText}
+ )} +
+
+ ); +} + +export function CommentItem({ commentId }: PlateCommentProps) { + const comment = useCommentById(commentId); + if (!comment) return null; + + return ( + + + + ); +} diff --git a/@/components/plate-ui/comment-leaf.tsx b/@/components/plate-ui/comment-leaf.tsx new file mode 100644 index 0000000..e228f8b --- /dev/null +++ b/@/components/plate-ui/comment-leaf.tsx @@ -0,0 +1,49 @@ +'use client'; + +import React from 'react'; +import { + TCommentText, + useCommentLeaf, + useCommentLeafState, +} from '@udecode/plate-comments'; +import { PlateLeaf, PlateLeafProps, Value } from '@udecode/plate-common'; + +import { cn } from '../../lib/utils'; + +export function CommentLeaf({ + className, + ...props +}: PlateLeafProps) { + const { children, nodeProps, leaf } = props; + + const state = useCommentLeafState({ leaf }); + const { props: rootProps } = useCommentLeaf(state); + + // hide resolved comments + if (!state.commentCount) return <>{children}; + + let aboveChildren = <>{children}; + + if (!state.isActive) { + for (let i = 1; i < state.commentCount; i++) { + aboveChildren = {aboveChildren}; + } + } + + return ( + + {aboveChildren} + + ); +} diff --git a/@/components/plate-ui/comment-more-dropdown.tsx b/@/components/plate-ui/comment-more-dropdown.tsx new file mode 100644 index 0000000..039586d --- /dev/null +++ b/@/components/plate-ui/comment-more-dropdown.tsx @@ -0,0 +1,44 @@ +'use client'; + +import React from 'react'; +import { + useCommentDeleteButton, + useCommentDeleteButtonState, + useCommentEditButton, + useCommentEditButtonState, +} from '@udecode/plate-comments'; + +import { cn } from '../../lib/utils'; +import { Icons } from './icons'; +import { Button } from './button'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from './dropdown-menu'; + +export function CommentMoreDropdown() { + const editButtonState = useCommentEditButtonState(); + const editProps = useCommentEditButton(editButtonState); + const deleteButtonState = useCommentDeleteButtonState(); + const deleteProps = useCommentDeleteButton(deleteButtonState); + + return ( + + + + + + + Edit comment + + + Delete comment + + + + ); +} diff --git a/@/components/plate-ui/comment-reply-items.tsx b/@/components/plate-ui/comment-reply-items.tsx new file mode 100644 index 0000000..57a4d0f --- /dev/null +++ b/@/components/plate-ui/comment-reply-items.tsx @@ -0,0 +1,21 @@ +'use client'; + +import React from 'react'; +import { + SCOPE_ACTIVE_COMMENT, + useCommentReplies, +} from '@udecode/plate-comments'; + +import { CommentItem } from './comment-item'; + +export function CommentReplyItems() { + const commentReplies = useCommentReplies(SCOPE_ACTIVE_COMMENT); + + return ( + <> + {Object.keys(commentReplies).map((id) => ( + + ))} + + ); +} diff --git a/@/components/plate-ui/comment-resolve-button.tsx b/@/components/plate-ui/comment-resolve-button.tsx new file mode 100644 index 0000000..51d3db2 --- /dev/null +++ b/@/components/plate-ui/comment-resolve-button.tsx @@ -0,0 +1,30 @@ +'use client'; + +import React from 'react'; +import { + CommentResolveButton as CommentResolveButtonPrimitive, + useComment, +} from '@udecode/plate-comments'; + +import { cn } from '../../lib/utils'; +import { Icons } from './icons'; +import { buttonVariants } from './button'; + +export function CommentResolveButton() { + const comment = useComment()!; + + return ( + + {comment.isResolved ? ( + + ) : ( + + )} + + ); +} diff --git a/@/components/plate-ui/comment-value.tsx b/@/components/plate-ui/comment-value.tsx new file mode 100644 index 0000000..fcb5d0b --- /dev/null +++ b/@/components/plate-ui/comment-value.tsx @@ -0,0 +1,39 @@ +'use client'; + +import React from 'react'; +import { + CommentEditActions, + CommentEditTextarea, + useCommentValue, +} from '@udecode/plate-comments'; + +import { cn } from '../../lib/utils'; +import { buttonVariants } from './button'; +import { inputVariants } from './input'; + +export function CommentValue() { + const { textareaRef } = useCommentValue(); + + return ( +
+ + +
+ + Cancel + + + + Save + +
+
+ ); +} diff --git a/@/components/plate-ui/comments-popover.tsx b/@/components/plate-ui/comments-popover.tsx new file mode 100644 index 0000000..01fff71 --- /dev/null +++ b/@/components/plate-ui/comments-popover.tsx @@ -0,0 +1,63 @@ +'use client'; + +import React from 'react'; +import { + CommentProvider, + CommentsPositioner, + SCOPE_ACTIVE_COMMENT, + useFloatingCommentsContentState, + useFloatingCommentsState, +} from '@udecode/plate-comments'; +import { PortalBody } from '@udecode/plate-common'; + +import { cn } from '../../lib/utils'; +import { popoverVariants } from './popover'; + +import { CommentCreateForm } from './comment-create-form'; +import { CommentItem } from './comment-item'; +import { CommentReplyItems } from './comment-reply-items'; + +export type FloatingCommentsContentProps = { + disableForm?: boolean; +}; + +export function CommentsPopoverContent(props: FloatingCommentsContentProps) { + const { disableForm } = props; + + const { ref, activeCommentId, hasNoComment, myUserId } = + useFloatingCommentsContentState(); + + return ( + +
+ {!hasNoComment && ( + <> + + + + + )} + + {!!myUserId && !disableForm && } +
+
+ ); +} + +export function CommentsPopover() { + const { loaded, activeCommentId } = useFloatingCommentsState(); + + if (!loaded || !activeCommentId) return null; + + return ( + + + + + + ); +} diff --git a/@/components/plate-ui/dialog.tsx b/@/components/plate-ui/dialog.tsx new file mode 100644 index 0000000..36a504b --- /dev/null +++ b/@/components/plate-ui/dialog.tsx @@ -0,0 +1,123 @@ +'use client'; + +import * as React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; + +import { cn } from '../../lib/utils'; +import { Icons } from './icons'; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = ({ + className, + ...props +}: DialogPrimitive.DialogPortalProps) => ( + +); +DialogPortal.displayName = DialogPrimitive.Portal.displayName; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = 'DialogHeader'; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = 'DialogFooter'; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/@/components/plate-ui/draggable.tsx b/@/components/plate-ui/draggable.tsx new file mode 100644 index 0000000..5fd07b4 --- /dev/null +++ b/@/components/plate-ui/draggable.tsx @@ -0,0 +1,157 @@ +'use client'; + +import React, { forwardRef } from 'react'; +import { ClassNames, PlateElementProps, TEditor } from '@udecode/plate-common'; +import { + DragItemNode, + useDraggable, + useDraggableState, +} from '@udecode/plate-dnd'; +import { DropTargetMonitor } from 'react-dnd'; + +import { cn } from '../../lib/utils'; +import { Icons } from './icons'; + +import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip'; + +export interface DraggableProps + extends PlateElementProps, + ClassNames<{ + /** + * Block and gutter. + */ + blockAndGutter: string; + + /** + * Block. + */ + block: string; + + /** + * Gutter at the left side of the editor. + * It has the height of the block + */ + gutterLeft: string; + + /** + * Block toolbar wrapper in the gutter left. + * It has the height of a line of the block. + */ + blockToolbarWrapper: string; + + /** + * Block toolbar in the gutter. + */ + blockToolbar: string; + + blockWrapper: string; + + /** + * Button to dnd the block, in the block toolbar. + */ + dragHandle: string; + + /** + * Icon of the drag button, in the drag icon. + */ + dragIcon: string; + + /** + * Show a dropline above or below the block when dragging a block. + */ + dropLine: string; + }> { + /** + * Intercepts the drop handling. + * If `false` is returned, the default drop behavior is called after. + * If `true` is returned, the default behavior is not called. + */ + onDropHandler?: ( + editor: TEditor, + props: { + monitor: DropTargetMonitor; + dragItem: DragItemNode; + nodeRef: any; + id: string; + } + ) => boolean; +} + +const dragHandle = ( + + + + + Drag to move + +); + +const Draggable = forwardRef( + ({ className, classNames = {}, onDropHandler, ...props }, ref) => { + const { children, element } = props; + + const state = useDraggableState({ element, onDropHandler }); + const { dropLine, isDragging, isHovered } = state; + const { + groupProps, + droplineProps, + gutterLeftProps, + previewRef, + handleRef, + } = useDraggable(state); + + return ( +
+
+
+
+
+ {isHovered && dragHandle} +
+
+
+
+ +
+ {children} + + {!!dropLine && ( +
+ )} +
+
+ ); + } +); +Draggable.displayName = 'Draggable'; + +export { Draggable }; diff --git a/@/components/plate-ui/dropdown-menu.tsx b/@/components/plate-ui/dropdown-menu.tsx new file mode 100644 index 0000000..fa5f455 --- /dev/null +++ b/@/components/plate-ui/dropdown-menu.tsx @@ -0,0 +1,239 @@ +'use client'; + +import * as React from 'react'; +import { useCallback, useState } from 'react'; +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; +import { cva, VariantProps } from 'class-variance-authority'; + +import { cn } from '../../lib/utils'; +import { Icons } from './icons'; + +const DropdownMenu = DropdownMenuPrimitive.Root; + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; + +const DropdownMenuGroup = DropdownMenuPrimitive.Group; + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; + +const DropdownMenuSub = DropdownMenuPrimitive.Sub; + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)); +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName; + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName; + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +export const menuItemVariants = cva( + cn( + 'relative flex h-9 cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors', + 'focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50' + ), + { + variants: { + inset: { + true: 'pl-8', + }, + }, + } +); + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)); +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName; + +interface DropdownMenuRadioItemProps + extends React.ComponentPropsWithoutRef< + typeof DropdownMenuPrimitive.RadioItem + > { + hideIcon?: boolean; +} + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + DropdownMenuRadioItemProps +>(({ className, children, hideIcon, ...props }, ref) => ( + + {!hideIcon && ( + + + + + + )} + {children} + +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean; + } +>(({ className, inset, ...props }, ref) => ( + +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +function DropdownMenuShortcut({ + className, + ...props +}: React.HTMLAttributes) { + return ( + + ); +} +DropdownMenuShortcut.displayName = 'DropdownMenuShortcut'; + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +}; + +export const useOpenState = () => { + const [open, setOpen] = useState(false); + + const onOpenChange = useCallback( + (_value = !open) => { + setOpen(_value); + }, + [open] + ); + + return { + open, + onOpenChange, + }; +}; diff --git a/@/components/plate-ui/editor.tsx b/@/components/plate-ui/editor.tsx new file mode 100644 index 0000000..8bdf9f4 --- /dev/null +++ b/@/components/plate-ui/editor.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { PlateContent } from '@udecode/plate-common'; +import { cva } from 'class-variance-authority'; + +import { cn } from '../../lib/utils'; + +import type { PlateContentProps } from '@udecode/plate-common'; +import type { VariantProps } from 'class-variance-authority'; + +const editorVariants = cva( + cn( + 'relative overflow-x-auto whitespace-pre-wrap break-words', + 'min-h-[80px] w-full rounded-md bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none', + '[&_[data-slate-placeholder]]:text-muted-foreground [&_[data-slate-placeholder]]:!opacity-100', + '[&_[data-slate-placeholder]]:top-[auto_!important]', + '[&_strong]:font-bold' + ), + { + variants: { + variant: { + outline: 'border border-input', + ghost: '', + }, + focused: { + true: 'ring-2 ring-ring ring-offset-2', + }, + disabled: { + true: 'cursor-not-allowed opacity-50', + }, + focusRing: { + true: 'focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', + false: '', + }, + size: { + sm: 'text-sm', + md: 'text-base', + }, + }, + defaultVariants: { + variant: 'outline', + focusRing: true, + size: 'sm', + }, + } +); + +export type EditorProps = PlateContentProps & + VariantProps; + +const Editor = React.forwardRef( + ( + { + className, + disabled, + focused, + focusRing, + readOnly, + size, + variant, + ...props + }, + ref + ) => { + return ( +
+ +
+ ); + } +); +Editor.displayName = 'Editor'; + +export { Editor }; diff --git a/@/components/plate-ui/emoji-combobox.tsx b/@/components/plate-ui/emoji-combobox.tsx new file mode 100644 index 0000000..9bc4e95 --- /dev/null +++ b/@/components/plate-ui/emoji-combobox.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { ComboboxItemProps } from '@udecode/plate-combobox'; +import { + EmojiItemData, + KEY_EMOJI, + TEmojiCombobox, + useEmojiComboboxState, +} from '@udecode/plate-emoji'; + +import { Combobox } from './combobox'; + +export function EmojiComboboxItem({ item }: ComboboxItemProps) { + const { + data: { id, emoji }, + } = item; + + return ( +
+ {emoji} :{id}: +
+ ); +} + +export function EmojiCombobox({ + pluginKey = KEY_EMOJI, + id = pluginKey, + ...props +}: TEmojiCombobox) { + const { trigger, onSelectItem } = useEmojiComboboxState({ pluginKey }); + + return ( + + ); +} diff --git a/@/components/plate-ui/excalidraw-element.tsx b/@/components/plate-ui/excalidraw-element.tsx new file mode 100644 index 0000000..4b4d6cf --- /dev/null +++ b/@/components/plate-ui/excalidraw-element.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { PlateElement, PlateElementProps, Value } from '@udecode/plate-common'; +import { + TExcalidrawElement, + useExcalidrawElement, +} from '@udecode/plate-excalidraw'; + +export function ExcalidrawElement({ + nodeProps, + ...props +}: PlateElementProps) { + const { children, element } = props; + + const { Excalidraw, excalidrawProps } = useExcalidrawElement({ + element, + }); + + return ( + +
+
+ {Excalidraw && ( + + )} +
+
+ {children} +
+ ); +} diff --git a/@/components/plate-ui/fixed-toolbar-buttons.tsx b/@/components/plate-ui/fixed-toolbar-buttons.tsx new file mode 100644 index 0000000..5a1e002 --- /dev/null +++ b/@/components/plate-ui/fixed-toolbar-buttons.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { + MARK_BOLD, + MARK_CODE, + MARK_ITALIC, + MARK_STRIKETHROUGH, + MARK_UNDERLINE, +} from '@udecode/plate-basic-marks'; +import { useEditorReadOnly } from '@udecode/plate-common'; + +import { Icons } from './icons'; + +import { InsertDropdownMenu } from './insert-dropdown-menu'; +import { MarkToolbarButton } from './mark-toolbar-button'; +import { ModeDropdownMenu } from './mode-dropdown-menu'; +import { ToolbarGroup } from './toolbar'; +import { TurnIntoDropdownMenu } from './turn-into-dropdown-menu'; + +export function FixedToolbarButtons() { + const readOnly = useEditorReadOnly(); + + return ( +
+
+ {!readOnly && ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + )} + +
+ + + + +
+
+ ); +} diff --git a/@/components/plate-ui/fixed-toolbar.tsx b/@/components/plate-ui/fixed-toolbar.tsx new file mode 100644 index 0000000..a2abcb6 --- /dev/null +++ b/@/components/plate-ui/fixed-toolbar.tsx @@ -0,0 +1,23 @@ +import React from 'react'; + +import { cn } from '../../lib/utils'; + +import { Toolbar, ToolbarProps } from './toolbar'; + +const FixedToolbar = React.forwardRef( + ({ className, ...props }: ToolbarProps, ref) => { + return ( + + ); + } +); +FixedToolbar.displayName = 'FixedToolbar'; + +export { FixedToolbar }; diff --git a/@/components/plate-ui/floating-toolbar-buttons.tsx b/@/components/plate-ui/floating-toolbar-buttons.tsx new file mode 100644 index 0000000..a7a64d1 --- /dev/null +++ b/@/components/plate-ui/floating-toolbar-buttons.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { + MARK_BOLD, + MARK_CODE, + MARK_ITALIC, + MARK_STRIKETHROUGH, + MARK_UNDERLINE, +} from '@udecode/plate-basic-marks'; +import { useEditorReadOnly } from '@udecode/plate-common'; + +import { Icons } from './icons'; + +import { MarkToolbarButton } from './mark-toolbar-button'; +import { MoreDropdownMenu } from './more-dropdown-menu'; +import { TurnIntoDropdownMenu } from './turn-into-dropdown-menu'; + +export function FloatingToolbarButtons() { + const readOnly = useEditorReadOnly(); + + return ( + <> + {!readOnly && ( + <> + + + + + + + + + + + + + + + + + + + )} + + + + ); +} diff --git a/@/components/plate-ui/floating-toolbar.tsx b/@/components/plate-ui/floating-toolbar.tsx new file mode 100644 index 0000000..1f54572 --- /dev/null +++ b/@/components/plate-ui/floating-toolbar.tsx @@ -0,0 +1,72 @@ +'use client'; + +import React from 'react'; +import { PortalBody, useComposedRef } from '@udecode/plate-common'; +import { + flip, + FloatingToolbarState, + offset, + useFloatingToolbar, + useFloatingToolbarState, +} from '@udecode/plate-floating'; + +import { cn } from '../../lib/utils'; + +import { Toolbar, ToolbarProps } from './toolbar'; + +export interface FloatingToolbarProps extends ToolbarProps { + state?: FloatingToolbarState; +} + +const FloatingToolbar = React.forwardRef< + React.ElementRef, + FloatingToolbarProps +>(({ state, children, ...props }, componentRef) => { + const floatingToolbarState = useFloatingToolbarState({ + ...state, + floatingOptions: { + placement: 'top', + middleware: [ + offset(12), + flip({ + padding: 12, + fallbackPlacements: [ + 'top-start', + 'top-end', + 'bottom-start', + 'bottom-end', + ], + }), + ], + ...state?.floatingOptions, + }, + }); + + const { + ref: floatingRef, + props: rootProps, + hidden, + } = useFloatingToolbar(floatingToolbarState); + + const ref = useComposedRef(componentRef, floatingRef); + + if (hidden) return null; + + return ( + + + {children} + + + ); +}); +FloatingToolbar.displayName = 'FloatingToolbar'; + +export { FloatingToolbar }; diff --git a/@/components/plate-ui/heading-element.tsx b/@/components/plate-ui/heading-element.tsx new file mode 100644 index 0000000..7235ae1 --- /dev/null +++ b/@/components/plate-ui/heading-element.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { PlateElement, PlateElementProps } from '@udecode/plate-common'; +import { cva, VariantProps } from 'class-variance-authority'; + +const headingVariants = cva('', { + variants: { + variant: { + h1: 'mb-1 mt-[2em] font-heading text-4xl font-bold', + h2: 'mb-px mt-[1.4em] font-heading text-2xl font-semibold tracking-tight', + h3: 'mb-px mt-[1em] font-heading text-xl font-semibold tracking-tight', + h4: 'mt-[0.75em] font-heading text-lg font-semibold tracking-tight', + h5: 'mt-[0.75em] text-lg font-semibold tracking-tight', + h6: 'mt-[0.75em] text-base font-semibold tracking-tight', + }, + isFirstBlock: { + true: 'mt-0', + false: '', + }, + }, +}); + +export function HeadingElement({ + className, + variant = 'h1', + isFirstBlock, + children, + ...props +}: PlateElementProps & VariantProps) { + const { element, editor } = props; + + const Element = variant!; + + return ( + + {children} + + ); +} diff --git a/@/components/plate-ui/highlight-leaf.tsx b/@/components/plate-ui/highlight-leaf.tsx new file mode 100644 index 0000000..a3ba121 --- /dev/null +++ b/@/components/plate-ui/highlight-leaf.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { PlateLeaf, PlateLeafProps } from '@udecode/plate-common'; + +import { cn } from '../../lib/utils'; + +export function HighlightLeaf({ + className, + children, + ...props +}: PlateLeafProps) { + return ( + + {children} + + ); +} diff --git a/@/components/plate-ui/hr-element.tsx b/@/components/plate-ui/hr-element.tsx new file mode 100644 index 0000000..a5e89a8 --- /dev/null +++ b/@/components/plate-ui/hr-element.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { PlateElement, PlateElementProps } from '@udecode/plate-common'; +import { useFocused, useSelected } from 'slate-react'; + +import { cn } from '../../lib/utils'; + +const HrElement = React.forwardRef< + React.ElementRef, + PlateElementProps +>(({ className, nodeProps, ...props }, ref) => { + const { children } = props; + + const selected = useSelected(); + const focused = useFocused(); + + return ( + +
+
+
+ {children} +
+ ); +}); +HrElement.displayName = 'HrElement'; + +export { HrElement }; diff --git a/@/components/plate-ui/image-element.tsx b/@/components/plate-ui/image-element.tsx new file mode 100644 index 0000000..b09c58b --- /dev/null +++ b/@/components/plate-ui/image-element.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { PlateElement, PlateElementProps, Value } from '@udecode/plate-common'; +import { + ELEMENT_IMAGE, + Image, + TImageElement, + useMediaState, +} from '@udecode/plate-media'; +import { useResizableStore } from '@udecode/plate-resizable'; + +import { cn } from '../../lib/utils'; + +import { Caption, CaptionTextarea } from './caption'; +import { MediaPopover } from './media-popover'; +import { + mediaResizeHandleVariants, + Resizable, + ResizeHandle, +} from './resizable'; + +export function ImageElement({ + className, + children, + nodeProps, + ...props +}: PlateElementProps) { + const { readOnly, focused, selected, align = 'center' } = useMediaState(); + const width = useResizableStore().get.width(); + + return ( + + +
+ + + + + + + + + +
+ + {children} +
+
+ ); +} diff --git a/@/components/plate-ui/input.tsx b/@/components/plate-ui/input.tsx new file mode 100644 index 0000000..ee5ff92 --- /dev/null +++ b/@/components/plate-ui/input.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import { cva, VariantProps } from 'class-variance-authority'; + +import { cn } from '../../lib/utils'; + +export const inputVariants = cva( + 'flex w-full rounded-md bg-transparent text-sm file:border-0 file:bg-background file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50', + { + variants: { + variant: { + default: + 'border border-input ring-offset-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2', + ghost: 'border-none focus-visible:ring-transparent', + }, + h: { + sm: 'h-9 px-3 py-2', + md: 'h-10 px-3 py-2', + }, + }, + defaultVariants: { + variant: 'default', + h: 'md', + }, + } +); + +export interface InputProps + extends React.InputHTMLAttributes, + VariantProps {} + +const Input = React.forwardRef( + ({ className, variant, type, ...props }, ref) => { + return ( + + ); + } +); +Input.displayName = 'Input'; + +export { Input }; diff --git a/@/components/plate-ui/insert-dropdown-menu.tsx b/@/components/plate-ui/insert-dropdown-menu.tsx new file mode 100644 index 0000000..31e7c97 --- /dev/null +++ b/@/components/plate-ui/insert-dropdown-menu.tsx @@ -0,0 +1,222 @@ +'use client'; + +import React from 'react'; +import { DropdownMenuProps } from '@radix-ui/react-dropdown-menu'; +import { ELEMENT_BLOCKQUOTE } from '@udecode/plate-block-quote'; +import { + focusEditor, + insertEmptyElement, + useEditorState, +} from '@udecode/plate-common'; +import { ELEMENT_H1, ELEMENT_H2, ELEMENT_H3 } from '@udecode/plate-heading'; +import { ELEMENT_PARAGRAPH } from '@udecode/plate-paragraph'; + +import { Icons } from './icons'; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, + useOpenState, +} from './dropdown-menu'; +import { ToolbarButton } from './toolbar'; + +const items = [ + { + label: 'Basic blocks', + items: [ + { + value: ELEMENT_PARAGRAPH, + label: 'Paragraph', + description: 'Paragraph', + icon: Icons.paragraph, + }, + { + value: ELEMENT_H1, + label: 'Heading 1', + description: 'Heading 1', + icon: Icons.h1, + }, + { + value: ELEMENT_H2, + label: 'Heading 2', + description: 'Heading 2', + icon: Icons.h2, + }, + { + value: ELEMENT_H3, + label: 'Heading 3', + description: 'Heading 3', + icon: Icons.h3, + }, + { + value: ELEMENT_BLOCKQUOTE, + label: 'Quote', + description: 'Quote (⌘+⇧+.)', + icon: Icons.blockquote, + }, + // { + // value: ELEMENT_TABLE, + // label: 'Table', + // description: 'Table', + // icon: Icons.table, + // }, + // { + // value: 'ul', + // label: 'Bulleted list', + // description: 'Bulleted list', + // icon: Icons.ul, + // }, + // { + // value: 'ol', + // label: 'Numbered list', + // description: 'Numbered list', + // icon: Icons.ol, + // }, + // { + // value: ELEMENT_HR, + // label: 'Divider', + // description: 'Divider (---)', + // icon: Icons.hr, + // }, + ], + }, + // { + // label: 'Media', + // items: [ + // { + // value: ELEMENT_CODE_BLOCK, + // label: 'Code', + // description: 'Code (```)', + // icon: Icons.codeblock, + // }, + // { + // value: ELEMENT_IMAGE, + // label: 'Image', + // description: 'Image', + // icon: Icons.image, + // }, + // { + // value: ELEMENT_MEDIA_EMBED, + // label: 'Embed', + // description: 'Embed', + // icon: Icons.embed, + // }, + // { + // value: ELEMENT_EXCALIDRAW, + // label: 'Excalidraw', + // description: 'Excalidraw', + // icon: Icons.excalidraw, + // }, + // ], + // }, + // { + // label: 'Inline', + // items: [ + // { + // value: ELEMENT_LINK, + // label: 'Link', + // description: 'Link', + // icon: Icons.link, + // }, + // ], + // }, +]; + +export function InsertDropdownMenu(props: DropdownMenuProps) { + const editor = useEditorState(); + const openState = useOpenState(); + + return ( + + + + + + + + + {items.map(({ items: nestedItems, label }, index) => ( + + {index !== 0 && } + + {label} + {nestedItems.map( + ({ value: type, label: itemLabel, icon: Icon }) => ( + { + switch (type) { + // case ELEMENT_CODE_BLOCK: { + // insertEmptyCodeBlock(editor); + // + // break; + // } + // case ELEMENT_IMAGE: { + // await insertMedia(editor, { type: ELEMENT_IMAGE }); + // + // break; + // } + // case ELEMENT_MEDIA_EMBED: { + // await insertMedia(editor, { + // type: ELEMENT_MEDIA_EMBED, + // }); + // + // break; + // } + // case 'ul': + // case 'ol': { + // insertEmptyElement(editor, ELEMENT_PARAGRAPH, { + // select: true, + // nextBlock: true, + // }); + // + // if (settingsStore.get.checkedId(KEY_LIST_STYLE_TYPE)) { + // toggleIndentList(editor, { + // listStyleType: type === 'ul' ? 'disc' : 'decimal', + // }); + // } else if (settingsStore.get.checkedId('list')) { + // toggleList(editor, { type }); + // } + // + // break; + // } + // case ELEMENT_TABLE: { + // insertTable(editor); + // + // break; + // } + // case ELEMENT_LINK: { + // triggerFloatingLink(editor, { focused: true }); + // + // break; + // } + default: { + insertEmptyElement(editor, type, { + select: true, + nextBlock: true, + }); + } + } + + focusEditor(editor); + }} + > + + {itemLabel} + + ) + )} + + ))} + + + ); +} diff --git a/@/components/plate-ui/kbd-leaf.tsx b/@/components/plate-ui/kbd-leaf.tsx new file mode 100644 index 0000000..c4e8157 --- /dev/null +++ b/@/components/plate-ui/kbd-leaf.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { PlateLeaf, PlateLeafProps } from '@udecode/plate-common'; + +import { cn } from '../../lib/utils'; + +export function KbdLeaf({ className, children, ...props }: PlateLeafProps) { + return ( + + {children} + + ); +} diff --git a/@/components/plate-ui/link-element.tsx b/@/components/plate-ui/link-element.tsx new file mode 100644 index 0000000..00acc10 --- /dev/null +++ b/@/components/plate-ui/link-element.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { PlateElement, PlateElementProps, Value } from '@udecode/plate-common'; +import { TLinkElement, useLink } from '@udecode/plate-link'; + +import { cn } from '../../lib/utils'; + +const LinkElement = React.forwardRef< + React.ElementRef, + PlateElementProps +>(({ className, children, ...props }, ref) => { + const { props: linkProps } = useLink({ element: props.element }); + + return ( + + {children} + + ); +}); +LinkElement.displayName = 'LinkElement'; + +export { LinkElement }; diff --git a/@/components/plate-ui/link-floating-toolbar.tsx b/@/components/plate-ui/link-floating-toolbar.tsx new file mode 100644 index 0000000..4c3f61f --- /dev/null +++ b/@/components/plate-ui/link-floating-toolbar.tsx @@ -0,0 +1,158 @@ +'use client'; + +import React from 'react'; +import { + flip, + offset, + UseVirtualFloatingOptions, +} from '@udecode/plate-floating'; +import { + FloatingLinkUrlInput, + LinkFloatingToolbarState, + LinkOpenButton, + useFloatingLinkEdit, + useFloatingLinkEditState, + useFloatingLinkInsert, + useFloatingLinkInsertState, +} from '@udecode/plate-link'; + +import { cn } from '../../lib/utils'; +import { Icons } from './icons'; + +import { buttonVariants } from './button'; +import { inputVariants } from './input'; +import { popoverVariants } from './popover'; +import { Separator } from './separator'; + +const floatingOptions: UseVirtualFloatingOptions = { + placement: 'bottom-start', + middleware: [ + offset(12), + flip({ + padding: 12, + fallbackPlacements: ['bottom-end', 'top-start', 'top-end'], + }), + ], +}; + +export interface LinkFloatingToolbarProps { + state?: LinkFloatingToolbarState; +} + +export function LinkFloatingToolbar({ state }: LinkFloatingToolbarProps) { + const insertState = useFloatingLinkInsertState({ + ...state, + floatingOptions: { + ...floatingOptions, + ...state?.floatingOptions, + }, + }); + const { + props: insertProps, + ref: insertRef, + hidden, + textInputProps, + } = useFloatingLinkInsert(insertState); + + const editState = useFloatingLinkEditState({ + ...state, + floatingOptions: { + ...floatingOptions, + ...state?.floatingOptions, + }, + }); + const { + props: editProps, + ref: editRef, + editButtonProps, + unlinkButtonProps, + } = useFloatingLinkEdit(editState); + + if (hidden) return null; + + const input = ( +
+
+
+ +
+ + +
+ + + +
+
+ +
+ +
+
+ ); + + const editContent = editState.isEditing ? ( + input + ) : ( +
+ + + + + + + + + + + +
+ ); + + return ( + <> +
+ {input} +
+ +
+ {editContent} +
+ + ); +} diff --git a/@/components/plate-ui/mark-toolbar-button.tsx b/@/components/plate-ui/mark-toolbar-button.tsx new file mode 100644 index 0000000..dfc3edf --- /dev/null +++ b/@/components/plate-ui/mark-toolbar-button.tsx @@ -0,0 +1,29 @@ +'use client'; + +import React from 'react'; +import { + useMarkToolbarButton, + useMarkToolbarButtonState, +} from '@udecode/plate-common'; + +import { ToolbarButton, ToolbarButtonProps } from './toolbar'; + +export interface MarkToolbarButtonProps + extends Pick { + nodeType: string; + clear?: string | string[]; +} + +/** + * Toolbar button to toggle the mark of the leaves in selection. + */ +export function MarkToolbarButton({ + clear, + nodeType, + ...props +}: MarkToolbarButtonProps) { + const state = useMarkToolbarButtonState({ clear, nodeType }); + const { props: buttonProps } = useMarkToolbarButton(state); + + return ; +} diff --git a/@/components/plate-ui/media-embed-element.tsx b/@/components/plate-ui/media-embed-element.tsx new file mode 100644 index 0000000..3159515 --- /dev/null +++ b/@/components/plate-ui/media-embed-element.tsx @@ -0,0 +1,141 @@ +import React from 'react'; +import { PlateElement, PlateElementProps, Value } from '@udecode/plate-common'; +import { + ELEMENT_MEDIA_EMBED, + parseTwitterUrl, + parseVideoUrl, + TMediaEmbedElement, + useMediaState, +} from '@udecode/plate-media'; +import { useResizableStore } from '@udecode/plate-resizable'; +import LiteYouTubeEmbed from 'react-lite-youtube-embed'; +import { Tweet } from 'react-tweet'; + +import { cn } from '../../lib/utils'; + +import { Caption, CaptionTextarea } from './caption'; +import { MediaPopover } from './media-popover'; +import { + mediaResizeHandleVariants, + Resizable, + ResizeHandle, +} from './resizable'; + +const MediaEmbedElement = React.forwardRef< + React.ElementRef, + PlateElementProps +>(({ className, children, ...props }, ref) => { + const { + align = 'center', + focused, + readOnly, + selected, + embed, + isTweet, + isVideo, + isYoutube, + } = useMediaState({ + urlParsers: [parseTwitterUrl, parseVideoUrl], + }); + const width = useResizableStore().get.width(); + const provider = embed?.provider; + + return ( + + +
+ + + + {isVideo ? ( + isYoutube ? ( + _iframe]:absolute [&_>_iframe]:left-0 [&_>_iframe]:top-0 [&_>_iframe]:h-full [&_>_iframe]:w-full', + '[&_>_.lty-playbtn]:z-[1] [&_>_.lty-playbtn]:h-[46px] [&_>_.lty-playbtn]:w-[70px] [&_>_.lty-playbtn]:rounded-[14%] [&_>_.lty-playbtn]:bg-[#212121] [&_>_.lty-playbtn]:opacity-80 [&_>_.lty-playbtn]:[transition:all_0.2s_cubic-bezier(0,_0,_0.2,_1)]', + '[&:hover_>_.lty-playbtn]:bg-[red] [&:hover_>_.lty-playbtn]:opacity-100', + '[&_>_.lty-playbtn]:before:border-y-[11px] [&_>_.lty-playbtn]:before:border-l-[19px] [&_>_.lty-playbtn]:before:border-r-0 [&_>_.lty-playbtn]:before:border-[transparent_transparent_transparent_#fff] [&_>_.lty-playbtn]:before:content-[""]', + '[&_>_.lty-playbtn]:absolute [&_>_.lty-playbtn]:left-1/2 [&_>_.lty-playbtn]:top-1/2 [&_>_.lty-playbtn]:[transform:translate3d(-50%,-50%,0)]', + '[&_>_.lty-playbtn]:before:absolute [&_>_.lty-playbtn]:before:left-1/2 [&_>_.lty-playbtn]:before:top-1/2 [&_>_.lty-playbtn]:before:[transform:translate3d(-50%,-50%,0)]', + '[&.lyt-activated]:cursor-[unset]', + '[&.lyt-activated]:before:pointer-events-none [&.lyt-activated]:before:opacity-0', + '[&.lyt-activated_>_.lty-playbtn]:pointer-events-none [&.lyt-activated_>_.lty-playbtn]:!opacity-0' + )} + /> + ) : ( +
+