From 37e12a4791f42a74a27907b5ef7a6464b1cf9ecd Mon Sep 17 00:00:00 2001 From: Nate Kelley Date: Mon, 28 Jul 2025 11:35:20 -0600 Subject: [PATCH] Initial commit for editor --- .../ui/report/AppReport.stories.tsx | 66 ++++ .../src/components/ui/report/AppReport.tsx | 58 +++ .../components/ui/report/EditorContainer.tsx | 38 ++ .../components/ui/report/EditorContent.tsx | 30 ++ .../src/components/ui/report/FixedToolbar.tsx | 17 + .../ui/report/MarkToolbarButtons.tsx | 22 ++ apps/web/src/components/ui/report/Toolbar.tsx | 364 ++++++++++++++++++ .../web/src/components/ui/tooltip/Tooltip.tsx | 35 +- .../src/components/ui/tooltip/TooltipBase.tsx | 37 +- apps/web/src/components/ui/tooltip/index.ts | 1 + apps/web/src/styles/tailwind.css | 6 +- 11 files changed, 649 insertions(+), 25 deletions(-) create mode 100644 apps/web/src/components/ui/report/AppReport.stories.tsx create mode 100644 apps/web/src/components/ui/report/AppReport.tsx create mode 100644 apps/web/src/components/ui/report/EditorContainer.tsx create mode 100644 apps/web/src/components/ui/report/EditorContent.tsx create mode 100644 apps/web/src/components/ui/report/FixedToolbar.tsx create mode 100644 apps/web/src/components/ui/report/MarkToolbarButtons.tsx create mode 100644 apps/web/src/components/ui/report/Toolbar.tsx diff --git a/apps/web/src/components/ui/report/AppReport.stories.tsx b/apps/web/src/components/ui/report/AppReport.stories.tsx new file mode 100644 index 000000000..7aeff5e92 --- /dev/null +++ b/apps/web/src/components/ui/report/AppReport.stories.tsx @@ -0,0 +1,66 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import type { Value } from 'platejs'; +import { AppReport } from './AppReport'; + +const meta: Meta = { + title: 'UI/report/AppReport', + component: AppReport, + parameters: { + layout: 'fullscreen' + }, + decorators: [ + (Story) => ( +
+
+ +
+
+ ) + ], + tags: ['autodocs'], + argTypes: { + variant: { + control: { type: 'select' }, + options: ['default'] + }, + readonly: { + control: { type: 'boolean' } + }, + placeholder: { + control: { type: 'text' } + }, + className: { + control: { type: 'text' } + } + } +}; + +export default meta; +type Story = StoryObj; + +// Example value structure for Plate.js +const exampleValue: Value = [ + { + id: '1', + type: 'p', + children: [ + { + text: 'This is an example report with some content. You can edit this text if readonly is set to false.' + } + ] + }, + { + id: '2', + type: 'p', + children: [{ text: 'This component uses Plate.js for rich text editing capabilities.' }] + } +]; + +export const Default: Story = { + args: { + value: exampleValue, + placeholder: 'Start typing your report...', + readonly: false, + variant: 'default' + } +}; diff --git a/apps/web/src/components/ui/report/AppReport.tsx b/apps/web/src/components/ui/report/AppReport.tsx new file mode 100644 index 000000000..048968c13 --- /dev/null +++ b/apps/web/src/components/ui/report/AppReport.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import type { Value } from 'platejs'; +import { Plate, usePlateEditor } from 'platejs/react'; +import { BoldPlugin, ItalicPlugin, UnderlinePlugin } from '@platejs/basic-nodes/react'; +import { EditorContainer } from './EditorContainer'; +import { EditorContent } from './EditorContent'; + +interface AppReportProps { + value: Value; + placeholder?: string; + readonly?: boolean; + variant?: 'default'; + className?: string; + disabled?: boolean; + style?: React.CSSProperties; +} + +export const AppReport = React.memo( + ({ + value, + placeholder, + readonly, + variant = 'default', + className, + style, + disabled = false + }: AppReportProps) => { + const editor = usePlateEditor({ + plugins: [BoldPlugin, ItalicPlugin, UnderlinePlugin], + value: [] + }); + + return ( + + {/* + + B + + + I + + + U + + */} + + + + + ); + } +); + +AppReport.displayName = 'AppReport'; diff --git a/apps/web/src/components/ui/report/EditorContainer.tsx b/apps/web/src/components/ui/report/EditorContainer.tsx new file mode 100644 index 000000000..45dd17fdc --- /dev/null +++ b/apps/web/src/components/ui/report/EditorContainer.tsx @@ -0,0 +1,38 @@ +import { cn } from '@/lib/utils'; +import { PlateContainer } from 'platejs/react'; +import { cva, type VariantProps } from 'class-variance-authority'; + +interface EditorContainerProps { + className?: string; + variant?: 'default'; + readonly?: boolean; + disabled?: boolean; +} + +const editorContainerVariants = cva('relative cursor-text h-full p-4', { + variants: { + variant: { + default: 'bg-background' + }, + readonly: { + true: 'cursor-not-allowed' + } + }, + defaultVariants: { + variant: 'default' + } +}); + +export function EditorContainer({ + className, + variant, + readonly, + disabled, + ...props +}: React.ComponentProps<'div'> & + VariantProps & + EditorContainerProps) { + return ( + + ); +} diff --git a/apps/web/src/components/ui/report/EditorContent.tsx b/apps/web/src/components/ui/report/EditorContent.tsx new file mode 100644 index 000000000..f866e152b --- /dev/null +++ b/apps/web/src/components/ui/report/EditorContent.tsx @@ -0,0 +1,30 @@ +import { PlateContent } from 'platejs/react'; +import { cn } from '@/lib/utils'; +import { cva } from 'class-variance-authority'; + +const editorContentVariants = cva('pb-42', { + variants: { + variant: { + default: 'bg-background' + } + } +}); + +export function EditorContent({ + style, + placeholder, + disabled, + variant, + ...props +}: React.ComponentProps & { variant?: 'default' }) { + return ( + + ); +} diff --git a/apps/web/src/components/ui/report/FixedToolbar.tsx b/apps/web/src/components/ui/report/FixedToolbar.tsx new file mode 100644 index 000000000..5537fb05e --- /dev/null +++ b/apps/web/src/components/ui/report/FixedToolbar.tsx @@ -0,0 +1,17 @@ +'use client'; + +import { cn } from '@/lib/utils'; + +import { Toolbar } from './Toolbar'; + +export function FixedToolbar({ className, ...props }: React.ComponentProps) { + return ( + + ); +} diff --git a/apps/web/src/components/ui/report/MarkToolbarButtons.tsx b/apps/web/src/components/ui/report/MarkToolbarButtons.tsx new file mode 100644 index 000000000..306b933f8 --- /dev/null +++ b/apps/web/src/components/ui/report/MarkToolbarButtons.tsx @@ -0,0 +1,22 @@ +'use client'; + +import * as React from 'react'; + +import { useMarkToolbarButton, useMarkToolbarButtonState } from 'platejs/react'; + +import { ToolbarButton } from './Toolbar'; +import { AppTooltip } from '../tooltip'; + +export function MarkToolbarButton({ + clear, + nodeType, + ...props +}: React.ComponentProps & { + nodeType: string; + clear?: string[] | string; +}) { + const state = useMarkToolbarButtonState({ clear, nodeType }); + const { props: buttonProps } = useMarkToolbarButton(state); + + return ; +} diff --git a/apps/web/src/components/ui/report/Toolbar.tsx b/apps/web/src/components/ui/report/Toolbar.tsx new file mode 100644 index 000000000..847e3720b --- /dev/null +++ b/apps/web/src/components/ui/report/Toolbar.tsx @@ -0,0 +1,364 @@ +'use client'; + +import * as React from 'react'; + +import * as ToolbarPrimitive from '@radix-ui/react-toolbar'; +import * as TooltipPrimitive from '@radix-ui/react-tooltip'; +import { type VariantProps, cva } from 'class-variance-authority'; +import { ChevronDown } from '../icons'; + +import { + DropdownMenuLabel, + DropdownMenuRadioGroup, + DropdownMenuSeparator +} from '@/components/ui/dropdown-menu'; +import { Separator } from '@/components/ui/separator'; +import { AppTooltip, TooltipBase as Tooltip, TooltipTrigger } from '@/components/ui/tooltip'; +import { cn } from '@/lib/utils'; + +export function Toolbar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export function ToolbarToggleGroup({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export function ToolbarLink({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +export function ToolbarSeparator({ + className, + ...props +}: React.ComponentProps) { + return ( + + ); +} + +// From toggleVariants +const toolbarButtonVariants = cva( + "inline-flex cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground disabled:pointer-events-none disabled:opacity-50 aria-checked:bg-accent aria-checked:text-accent-foreground aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + defaultVariants: { + size: 'default', + variant: 'default' + }, + variants: { + size: { + default: 'h-9 min-w-9 px-2', + lg: 'h-10 min-w-10 px-2.5', + sm: 'h-8 min-w-8 px-1.5' + }, + variant: { + default: 'bg-transparent', + outline: + 'border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground' + } + } + } +); + +const dropdownArrowVariants = cva( + cn( + 'inline-flex items-center justify-center rounded-r-md text-sm font-medium text-foreground transition-colors disabled:pointer-events-none disabled:opacity-50' + ), + { + defaultVariants: { + size: 'sm', + variant: 'default' + }, + variants: { + size: { + default: 'h-9 w-6', + lg: 'h-10 w-8', + sm: 'h-8 w-4' + }, + variant: { + default: + 'bg-transparent hover:bg-muted hover:text-muted-foreground aria-checked:bg-accent aria-checked:text-accent-foreground', + outline: + 'border border-l-0 border-input bg-transparent hover:bg-accent hover:text-accent-foreground' + } + } + } +); + +type ToolbarButtonProps = { + isDropdown?: boolean; + pressed?: boolean; +} & Omit, 'asChild' | 'value'> & + VariantProps; + +export const ToolbarButton = withTooltip(function ToolbarButton({ + children, + className, + isDropdown, + pressed, + size = 'sm', + variant, + ...props +}: ToolbarButtonProps) { + return typeof pressed === 'boolean' ? ( + + + {isDropdown ? ( + <> +
{children}
+
+ +
+ + ) : ( + children + )} +
+
+ ) : ( + + {children} + + ); +}); + +export function ToolbarSplitButton({ + className, + ...props +}: React.ComponentPropsWithoutRef) { + return ( + + ); +} + +type ToolbarSplitButtonPrimaryProps = Omit< + React.ComponentPropsWithoutRef, + 'value' +> & + VariantProps; + +export function ToolbarSplitButtonPrimary({ + children, + className, + size = 'sm', + variant, + ...props +}: ToolbarSplitButtonPrimaryProps) { + return ( + + {children} + + ); +} + +export function ToolbarSplitButtonSecondary({ + className, + size, + variant, + ...props +}: React.ComponentPropsWithoutRef<'span'> & VariantProps) { + return ( + e.stopPropagation()} + role="button" + {...props}> +
+ +
+
+ ); +} + +export function ToolbarToggleItem({ + className, + size = 'sm', + variant, + ...props +}: React.ComponentProps & + VariantProps) { + return ( + + ); +} + +export function ToolbarGroup({ children, className }: React.ComponentProps<'div'>) { + return ( + + ); +} + +type TooltipProps = { + tooltip?: React.ReactNode; + tooltipContentProps?: Omit, 'children'>; + tooltipProps?: Omit, 'children'>; + tooltipTriggerProps?: React.ComponentPropsWithoutRef; +} & React.ComponentProps; + +function withTooltip(Component: T) { + return function ExtendComponent({ + tooltip, + tooltipContentProps, + tooltipProps, + tooltipTriggerProps, + ...props + }: TooltipProps) { + const [mounted, setMounted] = React.useState(false); + + React.useEffect(() => { + setMounted(true); + }, []); + + const component = )} />; + + if (tooltip && mounted) { + return ( + + {component} + + // + // + // {component} + // + + // {tooltip} + // + ); + } + + return component; + }; +} + +function TooltipContent({ + children, + className, + // CHANGE + sideOffset = 4, + ...props +}: React.ComponentProps) { + return ( + + + {children} + {/* CHANGE */} + {/* */} + + + ); +} + +export function ToolbarMenuGroup({ + children, + className, + label, + ...props +}: React.ComponentProps & { label?: string }) { + return ( + <> + + + + {label && ( + + {label} + + )} + {children} + + + ); +} diff --git a/apps/web/src/components/ui/tooltip/Tooltip.tsx b/apps/web/src/components/ui/tooltip/Tooltip.tsx index 9ecab137f..1dea075a4 100644 --- a/apps/web/src/components/ui/tooltip/Tooltip.tsx +++ b/apps/web/src/components/ui/tooltip/Tooltip.tsx @@ -2,7 +2,7 @@ import omit from 'lodash/omit'; import React from 'react'; import { KeyboardShortcutPill } from '../pills/KeyboardShortcutPills'; import { - Tooltip as TooltipBase, + TooltipBase, TooltipContent as TooltipContentBase, TooltipProvider, TooltipTrigger @@ -41,22 +41,23 @@ export const Tooltip = React.memo( if (!title || (!title && !shortcuts?.length)) return children; return ( - - - - - {children} - - - - - - - + + + + {children} + + + + + + ); } ), diff --git a/apps/web/src/components/ui/tooltip/TooltipBase.tsx b/apps/web/src/components/ui/tooltip/TooltipBase.tsx index 463115e6f..7240dd2fd 100644 --- a/apps/web/src/components/ui/tooltip/TooltipBase.tsx +++ b/apps/web/src/components/ui/tooltip/TooltipBase.tsx @@ -4,19 +4,46 @@ import * as TooltipPrimitive from '@radix-ui/react-tooltip'; import * as React from 'react'; import { cn } from '@/lib/utils'; -const TooltipProvider = TooltipPrimitive.Provider; +function TooltipProvider({ + delayDuration = 0, + ...props +}: React.ComponentProps) { + return ( + + ); +} -const Tooltip = TooltipPrimitive.Root; +function TooltipBase({ + delayDuration, + skipDelayDuration, + ...props +}: React.ComponentProps & { + delayDuration?: number; + skipDelayDuration?: number; +}) { + return ( + + + + ); +} -const TooltipTrigger = TooltipPrimitive.Trigger; +function TooltipTrigger({ ...props }: React.ComponentProps) { + return ; +} const TooltipContent = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef ->(({ className, sideOffset = 4, ...props }, ref) => ( +>(({ className, sideOffset = 7, ...props }, ref) => (