diff --git a/app/(main)/(auth)/_components/SignInForm.tsx b/app/(main)/(auth)/_components/SignInForm.tsx index ebb80434..b5cfee93 100644 --- a/app/(main)/(auth)/_components/SignInForm.tsx +++ b/app/(main)/(auth)/_components/SignInForm.tsx @@ -1,9 +1,9 @@ -import { login } from '~/server/actions/auth'; -import { Input } from '~/components/ui/form/Input'; -import { SubmitButton } from '~/components/ui/form/SubmitButton'; -import Form from '~/components/ui/form/Form'; import { getTranslations } from 'next-intl/server'; +import Form from '~/components/form/Form'; +import { Input } from '~/components/form/Input'; +import { SubmitButton } from '~/components/form/SubmitButton'; import Link from '~/components/Link'; +import { login } from '~/server/actions/auth'; export default async function SignInForm() { const t = await getTranslations('Auth'); diff --git a/app/(main)/(auth)/_components/SignUpForm.tsx b/app/(main)/(auth)/_components/SignUpForm.tsx index 93c74678..fad7d1f2 100644 --- a/app/(main)/(auth)/_components/SignUpForm.tsx +++ b/app/(main)/(auth)/_components/SignUpForm.tsx @@ -1,11 +1,11 @@ 'use client'; +import { useTranslations } from 'next-intl'; import { useFormState } from 'react-dom'; +import Form from '~/components/form/Form'; +import { Input } from '~/components/form/Input'; +import { SubmitButton } from '~/components/form/SubmitButton'; import { signup } from '~/server/actions/auth'; -import { Input } from '~/components/ui/form/Input'; -import { useTranslations } from 'next-intl'; -import { SubmitButton } from '~/components/ui/form/SubmitButton'; -import Form from '~/components/ui/form/Form'; export default function SignUpForm() { const [formState, formAction] = useFormState(signup, { diff --git a/app/(main)/(dashboard)/_components/CreateStudyForm.tsx b/app/(main)/(dashboard)/_components/CreateStudyForm.tsx index e54d79a7..c73fbbf4 100644 --- a/app/(main)/(dashboard)/_components/CreateStudyForm.tsx +++ b/app/(main)/(dashboard)/_components/CreateStudyForm.tsx @@ -1,17 +1,17 @@ -import { createStudy } from '~/server/actions/study'; +import { Role } from '@prisma/client'; import { getTranslations } from 'next-intl/server'; -import { SubmitButton } from '~/components/ui/form/SubmitButton'; -import { Input } from '~/components/ui/form/Input'; +import Form from '~/components/form/Form'; +import { Input } from '~/components/form/Input'; +import { SubmitButton } from '~/components/form/SubmitButton'; import Section from '~/components/layout/Section'; import { Select, SelectContent, - SelectValue, - SelectTrigger, SelectItem, -} from '~/components/ui/select'; -import { Role } from '@prisma/client'; -import Form from '~/components/ui/form/Form'; + SelectTrigger, + SelectValue, +} from '~/components/select'; +import { createStudy } from '~/server/actions/study'; export default async function CreateStudyForm() { const t = await getTranslations('Components.CreateStudyForm'); diff --git a/app/(main)/(dashboard)/_components/SignOutBtn.tsx b/app/(main)/(dashboard)/_components/SignOutBtn.tsx index 49f7c985..4396b774 100644 --- a/app/(main)/(dashboard)/_components/SignOutBtn.tsx +++ b/app/(main)/(dashboard)/_components/SignOutBtn.tsx @@ -1,9 +1,8 @@ 'use client'; -import React from 'react'; -import { logout } from '~/server/actions/auth'; import { useTranslations } from 'next-intl'; -import { SubmitButton } from '~/components/ui/form/SubmitButton'; +import { SubmitButton } from '~/components/form/SubmitButton'; +import { logout } from '~/server/actions/auth'; export default function SignOutBtn() { const t = useTranslations('Auth.SignOut'); diff --git a/app/(main)/(dashboard)/_components/StudySwitcherClient.tsx b/app/(main)/(dashboard)/_components/StudySwitcherClient.tsx index c3d73afd..d2cfc092 100644 --- a/app/(main)/(dashboard)/_components/StudySwitcherClient.tsx +++ b/app/(main)/(dashboard)/_components/StudySwitcherClient.tsx @@ -1,8 +1,9 @@ 'use client'; import { type Study } from '@prisma/client'; -import { useParams, useRouter } from 'next/navigation'; import { useTranslations } from 'next-intl'; +import { useParams, useRouter } from 'next/navigation'; +import { route } from 'nextjs-routes'; import { useEffect, useState } from 'react'; import { Select, @@ -10,8 +11,7 @@ import { SelectItem, SelectTrigger, SelectValue, -} from '~/components/ui/select'; -import { route } from 'nextjs-routes'; +} from '~/components/select'; export default function StudySwitcherClient({ studies }: { studies: Study[] }) { const t = useTranslations('Components.StudySwitcher'); diff --git a/app/(main)/(dashboard)/layout.tsx b/app/(main)/(dashboard)/layout.tsx index 20fba013..2628fbba 100644 --- a/app/(main)/(dashboard)/layout.tsx +++ b/app/(main)/(dashboard)/layout.tsx @@ -1,12 +1,12 @@ import { SearchIcon } from 'lucide-react'; -import { Input } from '~/components/ui/form/Input'; import Image from 'next/image'; -import StudySwitcher from './_components/StudySwitcher'; import LanguageSwitcher from '~/app/_components/LocaleSwitcher'; -import SignOutBtn from './_components/SignOutBtn'; -import { requirePageAuth } from '~/lib/auth'; -import ResponsiveContainer from '~/components/layout/ResponsiveContainer'; import ThemeSwitcher from '~/app/_components/ThemeSwitcher'; +import { Input } from '~/components/form/Input'; +import ResponsiveContainer from '~/components/layout/ResponsiveContainer'; +import { requirePageAuth } from '~/lib/auth'; +import SignOutBtn from './_components/SignOutBtn'; +import StudySwitcher from './_components/StudySwitcher'; export default async function DashboardLayout({ children, diff --git a/app/(main)/(dashboard)/page.tsx b/app/(main)/(dashboard)/page.tsx index 7200f860..90af0e10 100644 --- a/app/(main)/(dashboard)/page.tsx +++ b/app/(main)/(dashboard)/page.tsx @@ -1,15 +1,15 @@ -import { getUserStudies } from '~/server/queries/studies'; import { getTranslations } from 'next-intl/server'; -import Link from '~/components/Link'; -import UnorderedList from '~/components/typography/UnorderedList'; +import { route } from 'nextjs-routes'; +import { Button } from '~/components/Button'; import Section from '~/components/layout/Section'; +import Link from '~/components/Link'; import PageHeader from '~/components/typography/PageHeader'; +import Paragraph from '~/components/typography/Paragraph'; +import UnorderedList from '~/components/typography/UnorderedList'; +import { requirePageAuth } from '~/lib/auth'; import { getInterviews } from '~/server/queries/interviews'; +import { getUserStudies } from '~/server/queries/studies'; import CreateStudyForm from './_components/CreateStudyForm'; -import { requirePageAuth } from '~/lib/auth'; -import { Button } from '~/components/ui/Button'; -import Paragraph from '~/components/typography/Paragraph'; -import { route } from 'nextjs-routes'; export default async function Dashboard() { await requirePageAuth(); diff --git a/app/_components/LocaleSwitcherSelect.tsx b/app/_components/LocaleSwitcherSelect.tsx index 019a6bb2..72694e04 100644 --- a/app/_components/LocaleSwitcherSelect.tsx +++ b/app/_components/LocaleSwitcherSelect.tsx @@ -1,14 +1,14 @@ 'use client'; +import { useTransition } from 'react'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from '~/components/ui/select'; +} from '~/components/select'; import { type Locale } from '~/lib/localisation/config'; -import { useTransition } from 'react'; import { setUserLocale } from '~/lib/localisation/locale'; import { getLocaleRecordsFromCodes } from '~/lib/localisation/utils'; diff --git a/app/_components/Providers.tsx b/app/_components/Providers.tsx index b3414e3b..c5968979 100644 --- a/app/_components/Providers.tsx +++ b/app/_components/Providers.tsx @@ -1,13 +1,13 @@ +import { MotionConfig } from 'framer-motion'; import { type AbstractIntlMessages } from 'next-intl'; -import { type ReactNode } from 'react'; -import RadixDirectionProvider from './RadixDirectionProvider'; -import { TooltipProvider } from '~/components/ui/Tooltip'; -import { type Locale } from '~/lib/localisation/config'; -import IntlProvider from './IntlProvider'; import { ThemeProvider } from 'next-themes'; -import { WizardProvider } from '~/lib/onboarding-wizard/Provider'; -import { MotionConfig } from 'framer-motion'; +import { type ReactNode } from 'react'; +import { TooltipProvider } from '~/components/Tooltip'; import DialogProvider from '~/lib/dialogs/DialogProvider'; +import { WizardProvider } from '~/lib/onboarding-wizard/Provider'; +import { type Locale } from '~/schemas/protocol/i18n'; +import IntlProvider from './IntlProvider'; +import RadixDirectionProvider from './RadixDirectionProvider'; export default function Providers({ intlParams: { dir, messages, locale, now, timeZone }, diff --git a/components/ui/Button.stories.tsx b/components/Button.stories.tsx similarity index 98% rename from components/ui/Button.stories.tsx rename to components/Button.stories.tsx index 064aad2a..527a6621 100644 --- a/components/ui/Button.stories.tsx +++ b/components/Button.stories.tsx @@ -1,12 +1,12 @@ import type { Meta } from '@storybook/react'; import { fn } from '@storybook/test'; +import Heading from '~/components/typography/Heading'; import { Button, type ButtonProps, type ButtonVariants, buttonVariants, } from './Button'; -import Heading from '../typography/Heading'; // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export const meta = { diff --git a/components/ui/Button.tsx b/components/Button.tsx similarity index 100% rename from components/ui/Button.tsx rename to components/Button.tsx diff --git a/components/ui/Card.stories.tsx b/components/Card.stories.tsx similarity index 94% rename from components/ui/Card.stories.tsx rename to components/Card.stories.tsx index 8a034d8c..ce2d2e7f 100644 --- a/components/ui/Card.stories.tsx +++ b/components/Card.stories.tsx @@ -1,9 +1,8 @@ // Card.stories.tsx -import React from 'react'; import type { Meta, StoryFn } from '@storybook/react'; +import Paragraph from '~/components/typography/Paragraph'; import { Card, type CardProps } from './Card'; -import Paragraph from '../typography/Paragraph'; // Meta configuration for Storybook const meta: Meta = { diff --git a/components/ui/Card.tsx b/components/Card.tsx similarity index 86% rename from components/ui/Card.tsx rename to components/Card.tsx index 866f8fc4..86b64a70 100644 --- a/components/ui/Card.tsx +++ b/components/Card.tsx @@ -1,11 +1,11 @@ 'use client'; import * as React from 'react'; -import { cn } from '~/lib/utils'; +import Divider from '~/components/layout/Divider'; +import Surface from '~/components/layout/Surface'; import Heading from '~/components/typography/Heading'; -import Paragraph from '../typography/Paragraph'; -import Surface from '../layout/Surface'; -import Divider from '../layout/Divider'; +import Paragraph from '~/components/typography/Paragraph'; +import { cn } from '~/lib/utils'; export type CardProps = { title: string; diff --git a/components/ui/CloseButton.tsx b/components/CloseButton.tsx similarity index 100% rename from components/ui/CloseButton.tsx rename to components/CloseButton.tsx diff --git a/components/DropdownMenu.stories.tsx b/components/DropdownMenu.stories.tsx new file mode 100644 index 00000000..9a76d8ca --- /dev/null +++ b/components/DropdownMenu.stories.tsx @@ -0,0 +1,41 @@ +import type { Meta, StoryFn } from '@storybook/react'; +import { Button } from './Button'; +import DropdownMenu from './DropdownMenu'; + +const meta: Meta = { + title: 'UI/DropdownMenu', + component: DropdownMenu.Root, +}; + +export default meta; + +export const Default: StoryFn = () => ( + + + + Choose an Option + + + + Input Control + + { + alert('selected checkbox'); + }} + > + Checkbox Group + + { + alert('selected toggle'); + }} + > + Toggle Button Group + + + + +); diff --git a/components/DropdownMenu.tsx b/components/DropdownMenu.tsx new file mode 100644 index 00000000..cfc3aa06 --- /dev/null +++ b/components/DropdownMenu.tsx @@ -0,0 +1,82 @@ +import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu'; +import { type PropsWithChildren } from 'react'; +import { cn } from '~/lib/utils'; +import Heading from './typography/Heading'; + +const DropdownTrigger = ({ children }: PropsWithChildren) => ( + + {children} + +); + +const DropdownMenuContent = ({ + children, + side = 'bottom', + container, +}: PropsWithChildren<{ + side?: 'top' | 'right' | 'bottom' | 'left'; + container?: React.RefObject; +}>) => { + // Use the portal prop if provided, otherwise fallback to the 'dialog-portal' element + const portalContainer = container?.current + ? container.current + : typeof window !== 'undefined' + ? document.getElementById('dialog-portal') + : null; + + return ( + + + {children} + + + ); +}; + +const DropdownItem = ({ + children, + textValue, + onSelect, + active, +}: PropsWithChildren<{ + textValue: string; + onSelect: () => void; + active?: boolean; +}>) => ( + + {children} + +); + +const DropdownLabel = ({ children }: PropsWithChildren) => ( + + {children} + +); + +const DropdownSeparator = () => ( + +); + +const DropdownMenu = { + Root: DropdownMenuPrimitive.Root, + RadioGroup: DropdownMenuPrimitive.RadioGroup, + Trigger: DropdownTrigger, + Content: DropdownMenuContent, + Label: DropdownLabel, + Item: DropdownItem, + Separator: DropdownSeparator, +}; + +export default DropdownMenu; diff --git a/components/ui/Popover.tsx b/components/Popover.tsx similarity index 92% rename from components/ui/Popover.tsx rename to components/Popover.tsx index 37ecd38a..8e86f38d 100644 --- a/components/ui/Popover.tsx +++ b/components/Popover.tsx @@ -1,20 +1,22 @@ -import { type PropsWithChildren } from 'react'; import * as PopoverPrimitive from '@radix-ui/react-popover'; +import { type PropsWithChildren } from 'react'; +import { MotionSurface } from '~/components/layout/Surface'; +import Heading from '~/components/typography/Heading'; import { cn } from '~/lib/utils'; import CloseButton from './CloseButton'; -import Heading from '../typography/Heading'; -import { MotionSurface } from '../layout/Surface'; const Popover = ({ children, title, content, modal, + side, isOpen, onOpenChange, }: PropsWithChildren<{ title?: string; content: string | React.ReactNode; + side?: 'top' | 'right' | 'bottom' | 'left'; modal?: boolean; isOpen?: boolean; onOpenChange?: (open: boolean) => void; @@ -50,6 +52,7 @@ const Popover = ({ // floating-ui prop exposed in @radix-ui/react-popper patch // see https://floating-ui.com/docs/flip#fallbackaxissidedirection fallbackAxisSideDirection="start" + side={side} > { + const [selectedToggle, setSelectedToggle] = useState(null); + + return ( + + alert('Button 1 clicked!')}> + Button + + { + setSelectedToggle(value ?? null); + }} + > + + Toggle 1 + + + Toggle 2 + + + + ); +}; + +export const Default: StoryFn = () => ( + }> + Open Toolbar + +); diff --git a/components/Toolbar.tsx b/components/Toolbar.tsx new file mode 100644 index 00000000..11e49a01 --- /dev/null +++ b/components/Toolbar.tsx @@ -0,0 +1,79 @@ +import * as ToolbarPrimitive from '@radix-ui/react-toolbar'; +import { type PropsWithChildren } from 'react'; +import { cn } from '~/lib/utils'; + +const ToolbarRoot = ({ children }: PropsWithChildren) => ( + + {children} + +); + +const ToolbarButton = ({ + children, + onClick, +}: { + children: React.ReactNode; + onClick?: () => void; +}) => ( + + + {children} + + +); + +const ToolbarToggleGroup = ({ + children, + type, + onValueChange, +}: PropsWithChildren<{ + type: 'single' | 'multiple'; + onValueChange?: (value?: string) => void; +}>) => ( + + {children} + +); + +const ToolbarToggleItem = ({ + value, + children, + active, + onClick, +}: { + value: string; + children: React.ReactNode; + active?: boolean; + onClick?: () => void; +}) => ( + + + {children} + + +); + +const ToolbarMenu = { + Root: ToolbarRoot, + Button: ToolbarButton, + ToggleGroup: ToolbarToggleGroup, + ToggleItem: ToolbarToggleItem, + Separator: ToolbarPrimitive.Separator, +}; + +export default ToolbarMenu; diff --git a/components/ui/Tooltip.stories.tsx b/components/Tooltip.stories.tsx similarity index 100% rename from components/ui/Tooltip.stories.tsx rename to components/Tooltip.stories.tsx diff --git a/components/ui/Tooltip.tsx b/components/Tooltip.tsx similarity index 100% rename from components/ui/Tooltip.tsx rename to components/Tooltip.tsx diff --git a/components/block-editor/BlockEditor.stories.tsx b/components/block-editor/BlockEditor.stories.tsx new file mode 100644 index 00000000..d720f573 --- /dev/null +++ b/components/block-editor/BlockEditor.stories.tsx @@ -0,0 +1,27 @@ +import type { Meta, StoryObj } from '@storybook/react'; +import BlockEditor from './BlockEditor'; + +const meta: Meta = { + title: 'Systems/BlockEditor', + component: BlockEditor, + parameters: { + nextjs: { + appDirectory: 'true', + }, + layout: 'fullscreen', + }, + decorators: [ + (Story, _context) => { + return ( + + + + ); + }, + ], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/components/block-editor/BlockEditor.tsx b/components/block-editor/BlockEditor.tsx new file mode 100644 index 00000000..fdc7c4e8 --- /dev/null +++ b/components/block-editor/BlockEditor.tsx @@ -0,0 +1,33 @@ +'use client'; + +import { EditorContent } from '@tiptap/react'; +import { useRef } from 'react'; +import GroupMenu from '~/lib/block-editor/extensions/Group/GroupMenu'; +import { NodeBubbleMenu } from '~/lib/block-editor/extensions/NodeBubbleMenu'; +import { useBlockEditor } from '~/lib/block-editor/useBlockEditor'; +import PlusMenu from './PlusMenu'; +import SidePanel from './SidePanel'; + +const BlockEditor = () => { + const { editor } = useBlockEditor(); + const menuContainerRef = useRef(null); + + return ( + + + + + + + + + + + + ); +}; + +export default BlockEditor; diff --git a/components/block-editor/BubbleMenu.tsx b/components/block-editor/BubbleMenu.tsx new file mode 100644 index 00000000..9da7cf64 --- /dev/null +++ b/components/block-editor/BubbleMenu.tsx @@ -0,0 +1,13 @@ +import { + BubbleMenu as BaseBubbleMenu, + type BubbleMenuProps, +} from '@tiptap/react'; + +export default function BubbleMenu(props: BubbleMenuProps) { + return ( + + ); +} diff --git a/components/block-editor/PlusMenu.tsx b/components/block-editor/PlusMenu.tsx new file mode 100644 index 00000000..ac6fb815 --- /dev/null +++ b/components/block-editor/PlusMenu.tsx @@ -0,0 +1,90 @@ +import { type Editor } from '@tiptap/react'; +import { Plus } from 'lucide-react'; +import { useState } from 'react'; +import { + type TiptapContent, + contentMap, +} from '~/lib/block-editor/contentTypes'; +import { createVariableNode } from '~/lib/block-editor/extensions/Variable/utils'; +import devProtocol from '~/lib/db/sample-data/dev-protocol'; +import { type TVariableDefinition } from '~/schemas/protocol/variables'; +import { Button } from '../Button'; +import DropdownMenu from '../DropdownMenu'; + +export default function PlusMenu({ editor }: { editor: Editor | null }) { + const [isOpen, setIsOpen] = useState(false); + + if (!editor) return null; + const { variables } = devProtocol; + + const handleCommand = (type: TiptapContent) => () => { + const endPos = editor.state.doc.content.size; + const value = contentMap[type]; + if (!value) return; + editor + .chain() + .focus() + .insertContentAt(endPos, value, { + updateSelection: true, + }) + .run(); + + setIsOpen(false); + }; + + const addVariable = (variable: TVariableDefinition) => { + const endPos = editor.state.doc.content.size; + const variableNode = createVariableNode({ + newVariable: variable, + view: editor.view, + key: variable.label.en ?? '', //TODO: better integration with translations + }); + + if (!variableNode) return; + const transaction = editor.view.state.tr.insert(endPos, variableNode); + editor.view.dispatch(transaction); + setIsOpen(false); + }; + + return ( + + + {isOpen ? ( + + ) : ( + + + + )} + + + Add content + {Object.keys(contentMap).map((type) => ( + + {type} + + ))} + + + + Add variable + {variables && + Object.entries(variables).map(([key, variable]) => ( + { + addVariable(variable); + }} + textValue={variable.label.en ?? ''} + > + {variable.label.en} + + ))} + + + ); +} diff --git a/components/block-editor/SidePanel.tsx b/components/block-editor/SidePanel.tsx new file mode 100644 index 00000000..65cc440c --- /dev/null +++ b/components/block-editor/SidePanel.tsx @@ -0,0 +1,72 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +import { + CaseSensitive, + Hash, + type LucideIcon, + SquareStack, +} from 'lucide-react'; +import React from 'react'; +import Heading from '~/components/typography/Heading'; +import { handleDrag } from '~/lib/block-editor/utils'; +import devProtocol from '~/lib/db/sample-data/dev-protocol'; +import { cn } from '~/lib/utils'; +import { Card } from '../Card'; + +const VARIABLE_ICONS: Record = { + text: CaseSensitive, + number: Hash, + categorical: SquareStack, +}; + +export const FORMATS = [ + { name: 'Paragraph', type: 'paragraph' }, + { name: 'H1', type: 'h1' }, + { name: 'H2', type: 'h2' }, + { name: 'H3', type: 'h3' }, + { name: 'H4', type: 'h4' }, + { name: 'Bullet List', type: 'bulletList' }, + { name: 'Group', type: 'group' }, +]; + +export default function SidePanel() { + const { variables } = devProtocol; + + if (!variables) return null; + + const renderDraggableItem = ( + label: string, + onDragStart: (e: React.DragEvent) => void, + Icon?: LucideIcon, + ) => ( + + {label} + {Icon && } + + ); + + return ( + + Variables + {Object.entries(variables).map(([key, variable]) => { + const Icon = VARIABLE_ICONS[variable.type]; + return renderDraggableItem( + variable.label.en, + (e) => handleDrag(e, 'variable', variable, key), + Icon, + ); + })} + + Content + {FORMATS.map(({ name, type }) => + renderDraggableItem(name, (e) => handleDrag(e, type)), + )} + + ); +} diff --git a/components/ui/form/Form.stories.tsx b/components/form/Form.stories.tsx similarity index 100% rename from components/ui/form/Form.stories.tsx rename to components/form/Form.stories.tsx diff --git a/components/ui/form/Form.tsx b/components/form/Form.tsx similarity index 100% rename from components/ui/form/Form.tsx rename to components/form/Form.tsx diff --git a/components/ui/form/Input.tsx b/components/form/Input.tsx similarity index 100% rename from components/ui/form/Input.tsx rename to components/form/Input.tsx diff --git a/components/ui/form/Label.tsx b/components/form/Label.tsx similarity index 92% rename from components/ui/form/Label.tsx rename to components/form/Label.tsx index 9bfcf60e..1ad5d3a9 100644 --- a/components/ui/form/Label.tsx +++ b/components/form/Label.tsx @@ -1,11 +1,11 @@ 'use client'; -import * as React from 'react'; import * as LabelPrimitive from '@radix-ui/react-label'; +import * as React from 'react'; import { type VariantProps } from 'tailwind-variants'; +import { headingVariants } from '~/components/typography/Heading'; import { cn } from '~/lib/utils'; -import { headingVariants } from '../../typography/Heading'; const Label = React.forwardRef< React.ElementRef, diff --git a/components/ui/form/RadioGroup.tsx b/components/form/RadioGroup.tsx similarity index 100% rename from components/ui/form/RadioGroup.tsx rename to components/form/RadioGroup.tsx diff --git a/components/ui/form/Select.tsx b/components/form/Select.tsx similarity index 96% rename from components/ui/form/Select.tsx rename to components/form/Select.tsx index bd617167..83c57731 100644 --- a/components/ui/form/Select.tsx +++ b/components/form/Select.tsx @@ -1,11 +1,11 @@ +import type * as SelectPrimitive from '@radix-ui/react-select'; import { Select, SelectContent, - SelectValue, - SelectTrigger, SelectItem, -} from '~/components/ui/select'; -import type * as SelectPrimitive from '@radix-ui/react-select'; + SelectTrigger, + SelectValue, +} from '~/components/select'; type SimpleSelectProps = { options: { label: string; value: string }[]; diff --git a/components/ui/form/SubmitButton.tsx b/components/form/SubmitButton.tsx similarity index 89% rename from components/ui/form/SubmitButton.tsx rename to components/form/SubmitButton.tsx index 8e3d5abc..5549f780 100644 --- a/components/ui/form/SubmitButton.tsx +++ b/components/form/SubmitButton.tsx @@ -2,7 +2,7 @@ import { Loader2 } from 'lucide-react'; import { useFormStatus } from 'react-dom'; -import { Button } from '~/components/ui/Button'; +import { Button } from '~/components/Button'; export function SubmitButton({ children }: React.PropsWithChildren) { const { pending } = useFormStatus(); diff --git a/components/ui/form/Switch.tsx b/components/form/Switch.tsx similarity index 100% rename from components/ui/form/Switch.tsx rename to components/form/Switch.tsx diff --git a/components/interview/interfaces/name-generator/NameGenerator.tsx b/components/interview/interfaces/name-generator/NameGenerator.tsx index 1075facd..b7f4a6e3 100644 --- a/components/interview/interfaces/name-generator/NameGenerator.tsx +++ b/components/interview/interfaces/name-generator/NameGenerator.tsx @@ -2,17 +2,15 @@ * Building blocks for NameGenerator interface */ -import Prompts from '~/components/interview/Prompts/Prompts'; -import NodePanels from './NodePanels'; import NodeList from '~/components/interview/NodeList'; -import QuickNodeForm from './QuickNodeForm'; -import { cn } from '~/lib/utils'; -import { interfaceWrapperClasses } from '../../ui/SimpleShell'; -import { withOnboardingWizard } from '~/components/onboard-wizard/withOnboardingWizard'; -import { type InterviewStage } from '../../ui/InterviewShell'; -import { useTranslations } from 'next-intl'; +import Prompts from '~/components/interview/Prompts/Prompts'; +import { type InterviewStage } from '~/components/interview/ui/InterviewShell'; +import { interfaceWrapperClasses } from '~/components/interview/ui/SimpleShell'; import devProtocol from '~/lib/db/sample-data/dev-protocol'; +import { cn } from '~/lib/utils'; import { type NameGeneratorInterface } from '~/schemas/protocol/interfaces/name-generator'; +import NodePanels from './NodePanels'; +import QuickNodeForm from './QuickNodeForm'; const demoNodes = [ { diff --git a/components/interview/ui/HelpButton.tsx b/components/interview/ui/HelpButton.tsx index 2b8e0244..a7408e9d 100644 --- a/components/interview/ui/HelpButton.tsx +++ b/components/interview/ui/HelpButton.tsx @@ -1,14 +1,14 @@ import { HelpCircle } from 'lucide-react'; -import { NavButtonWithTooltip } from './NavigationButton'; import { useTranslations } from 'next-intl'; +import { Button } from '~/components/Button'; +import { Card } from '~/components/Card'; +import Form from '~/components/form/Form'; import { useWizardController } from '~/components/onboard-wizard/useWizardController'; -import { env } from '~/env'; -import { WIZARD_LOCAL_STORAGE_KEY } from '~/lib/onboarding-wizard/Provider'; import { renderLocalisedValue } from '~/components/RenderRichText'; +import { env } from '~/env'; import { useDialog } from '~/lib/dialogs/DialogProvider'; -import { Button } from '~/components/ui/Button'; -import { Card } from '~/components/ui/Card'; -import Form from '~/components/ui/form/Form'; +import { WIZARD_LOCAL_STORAGE_KEY } from '~/lib/onboarding-wizard/Provider'; +import { NavButtonWithTooltip } from './NavigationButton'; /** * Button to be added to the main navigation, which triggers the help popover. * This popover allows the participant to trigger help wizards. diff --git a/components/interview/ui/InterviewLocaleSwitcher.tsx b/components/interview/ui/InterviewLocaleSwitcher.tsx index b23652ea..1d8b3c7b 100644 --- a/components/interview/ui/InterviewLocaleSwitcher.tsx +++ b/components/interview/ui/InterviewLocaleSwitcher.tsx @@ -1,12 +1,12 @@ import { SelectTrigger } from '@radix-ui/react-select'; +import { Globe } from 'lucide-react'; import { useLocale, useTranslations } from 'next-intl'; import { useTransition } from 'react'; -import { Select, SelectContent, SelectItem } from '~/components/ui/select'; +import { Select, SelectContent, SelectItem } from '~/components/select'; import { type Locale } from '~/lib/localisation/config'; import { setUserLocale } from '~/lib/localisation/locale'; import { getLocaleRecordsFromCodes } from '~/lib/localisation/utils'; import { NavButtonWithTooltip } from './NavigationButton'; -import { Globe } from 'lucide-react'; export default function InterviewLocaleSwitcher({ codes, diff --git a/components/interview/ui/Navigation.tsx b/components/interview/ui/Navigation.tsx index 6a8bc38e..17ec54e7 100644 --- a/components/interview/ui/Navigation.tsx +++ b/components/interview/ui/Navigation.tsx @@ -1,24 +1,23 @@ 'use client'; import { - ChevronUp, ChevronDown, ChevronLeft, ChevronRight, + ChevronUp, } from 'lucide-react'; import { useTranslations } from 'next-intl'; -import { useSearchParams } from 'next/navigation'; -import { usePathname, useRouter } from 'next/navigation'; -import { cn } from '~/lib/utils'; -import { ProgressBarWithTooltip } from '../../ui/ProgressBar'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import type { IntRange } from 'type-fest'; -import HelpButton from './HelpButton'; -import { NavButtonWithTooltip } from './NavigationButton'; +import { ProgressBarWithTooltip } from '~/components/ProgressBar'; import Surface from '~/components/layout/Surface'; -import { type Locale } from '~/lib/localisation/config'; +import { withOnboardingWizard } from '~/components/onboard-wizard/withOnboardingWizard'; import { useMediaQuery } from '~/hooks/useMediaQuery'; +import { cn } from '~/lib/utils'; +import { type Locale } from '~/schemas/protocol/i18n'; +import HelpButton from './HelpButton'; import InterviewLocaleSwitcher from './InterviewLocaleSwitcher'; -import { withOnboardingWizard } from '~/components/onboard-wizard/withOnboardingWizard'; +import { NavButtonWithTooltip } from './NavigationButton'; type NavigationProps = { pulseNext: boolean; diff --git a/components/interview/ui/NavigationButton.tsx b/components/interview/ui/NavigationButton.tsx index 9553a230..988c88c1 100644 --- a/components/interview/ui/NavigationButton.tsx +++ b/components/interview/ui/NavigationButton.tsx @@ -1,5 +1,5 @@ -import { Button, type ButtonProps } from '~/components/ui/Button'; -import { withTooltip } from '~/components/ui/Tooltip'; +import { Button, type ButtonProps } from '~/components/Button'; +import { withTooltip } from '~/components/Tooltip'; import { cn } from '~/lib/utils'; const NavigationButton = (props: ButtonProps) => { diff --git a/components/layout/Section.tsx b/components/layout/Section.tsx index f086fd89..c6964254 100644 --- a/components/layout/Section.tsx +++ b/components/layout/Section.tsx @@ -1,8 +1,8 @@ 'use client'; -import Heading from '../typography/Heading'; -import { cn } from '~/lib/utils'; import { useId } from 'react'; +import Heading from '~/components/typography/Heading'; +import { cn } from '~/lib/utils'; import Surface, { type SurfaceVariants } from './Surface'; const sectionClasses = 'rounded mb-10'; diff --git a/components/layout/Surface.stories.tsx b/components/layout/Surface.stories.tsx index 148be014..d666932c 100644 --- a/components/layout/Surface.stories.tsx +++ b/components/layout/Surface.stories.tsx @@ -1,9 +1,9 @@ // src/components/Surface.stories.tsx -import React, { type ElementType } from 'react'; import type { Meta, StoryFn } from '@storybook/react'; +import { type ElementType } from 'react'; +import { Button } from '~/components/Button'; import Surface, { MotionSurface, type SurfaceVariants } from './Surface'; -import { Button } from '../ui/Button'; // Define the metadata for the Storybook const meta: Meta = { diff --git a/components/onboard-wizard/WizardStep.tsx b/components/onboard-wizard/WizardStep.tsx index 0758c5ce..fa45d464 100644 --- a/components/onboard-wizard/WizardStep.tsx +++ b/components/onboard-wizard/WizardStep.tsx @@ -1,15 +1,14 @@ -import Popover from '~/components/ui/Popover'; -import { Button } from '../ui/Button'; -import { useWizardController } from './useWizardController'; -import RenderRichText from '../RenderRichText'; import { useTranslations } from 'next-intl'; -import { useElementPosition } from '~/lib/onboarding-wizard/utils'; -import { type Step } from '~/lib/onboarding-wizard/store'; -import Form from '../ui/form/Form'; -import { generatePublicId } from '~/lib/generatePublicId'; +import { Button } from '~/components/Button'; +import Form from '~/components/form/Form'; +import Popover from '~/components/Popover'; import { ControlledDialog } from '~/lib/dialogs/ControlledDialog'; +import { generatePublicId } from '~/lib/generatePublicId'; +import { useElementPosition } from '~/lib/onboarding-wizard/utils'; +import RenderRichText from '../RenderRichText'; +import { useWizardController } from './useWizardController'; -export default function WizardStep({ step }: { step: Step }) { +export default function WizardStep({ step }: { step }) { const { title, content, targetElementId } = step; const { diff --git a/components/ui/select.tsx b/components/select.tsx similarity index 100% rename from components/ui/select.tsx rename to components/select.tsx diff --git a/components/typography/Heading.tsx b/components/typography/Heading.tsx index 56ca1c48..8ae9beef 100644 --- a/components/typography/Heading.tsx +++ b/components/typography/Heading.tsx @@ -1,21 +1,21 @@ 'use client'; -import { tv, type VariantProps } from 'tailwind-variants'; +import { Slot } from '@radix-ui/react-slot'; import React from 'react'; +import { tv, type VariantProps } from 'tailwind-variants'; import { cn } from '~/lib/utils'; -import { Slot } from '@radix-ui/react-slot'; export const headingVariants = tv({ - base: 'text-balance font-heading font-bold [&:has(+.lead)]:mb-2 max-w-[55ch]', + base: 'font-heading max-w-[55ch] text-balance font-bold [&:has(+.lead)]:mb-2', variants: { variant: { - 'h1': 'scroll-m-20 text-2xl tracking-tight mb-6', - 'h2': 'scroll-m-20 text-xl tracking-tight mb-4', - 'h3': 'scroll-m-20 text-lg mb-3', - 'h4': 'scroll-m-20 text-base mb-2 leading-6 font-[460]', - 'h4-all-caps': 'scroll-m-20 text-base tracking-widest uppercase', + 'h1': 'mb-6 scroll-m-20 text-2xl tracking-tight', + 'h2': 'mb-4 scroll-m-20 text-xl tracking-tight', + 'h3': 'mb-3 scroll-m-20 text-lg', + 'h4': 'mb-2 scroll-m-20 text-base font-[460] leading-6', + 'h4-all-caps': 'scroll-m-20 text-base uppercase tracking-widest', 'label': - 'scroll-m-20 text-sm tracking-normal peer-disabled:opacity-70 peer-disabled:cursor-not-allowed font-extrabold', + 'scroll-m-20 text-sm font-extrabold tracking-normal peer-disabled:cursor-not-allowed peer-disabled:opacity-70', }, }, defaultVariants: { diff --git a/components/typography/UnorderedList.tsx b/components/typography/UnorderedList.tsx index 5cc51e24..9a2db876 100644 --- a/components/typography/UnorderedList.tsx +++ b/components/typography/UnorderedList.tsx @@ -1,5 +1,7 @@ import { cn } from '~/lib/utils'; +export const unorderedListClasses = 'my-3 ml-8 list-disc [&>li]:mt-1'; + export default function UnorderedList({ children, className, @@ -7,9 +9,5 @@ export default function UnorderedList({ children: React.ReactNode; className?: string; }) { - return ( - li]:mt-1', className)}> - {children} - - ); + return {children}; } diff --git a/lib/block-editor/contentTypes.ts b/lib/block-editor/contentTypes.ts new file mode 100644 index 00000000..9e15138f --- /dev/null +++ b/lib/block-editor/contentTypes.ts @@ -0,0 +1,49 @@ +import type { Content } from '@tiptap/react'; + +export type TiptapContent = + | 'h1' + | 'h2' + | 'h3' + | 'h4' + | 'paragraph' + | 'bulletList' + | 'group' + | 'variable'; + +export const contentMap: Record = { + h1: { + type: 'heading', + attrs: { level: 1 }, + }, + h2: { + type: 'heading', + attrs: { level: 2 }, + }, + h3: { + type: 'heading', + attrs: { level: 3 }, + }, + h4: { + type: 'heading', + attrs: { level: 4 }, + }, + paragraph: { type: 'paragraph' }, + bulletList: { + type: 'bulletList', + content: [ + { + type: 'listItem', + content: [ + { + type: 'paragraph', + }, + ], + }, + ], + }, + group: { + type: 'group', + attrs: { columns: 1 }, + content: [], + }, +}; diff --git a/lib/block-editor/extensions/Dropcursor/Dropcursor.tsx b/lib/block-editor/extensions/Dropcursor/Dropcursor.tsx new file mode 100644 index 00000000..4d8075e6 --- /dev/null +++ b/lib/block-editor/extensions/Dropcursor/Dropcursor.tsx @@ -0,0 +1,51 @@ +// adapted from tiptap dropcursor extension +// https://github.com/ueberdosis/tiptap/tree/main/packages/extension-dropcursor +// uses custom prosemirror-dropcursor replacement instead of prosemirror-dropcursor plugin + +import { Extension } from '@tiptap/core'; +import { dropCursor } from './pmDropcursorReplacement'; + +export type DropcursorOptions = { + /** + * The color of the drop cursor + * @default 'currentColor' + * @example 'red' + */ + color: string | undefined; + + /** + * The width of the drop cursor + * @default 1 + * @example 2 + */ + width: number | undefined; + + /** + * The class of the drop cursor + * @default undefined + * @example 'drop-cursor' + */ + class: string | undefined; +}; + +/** + * This extension allows you to add a drop cursor to your editor. + * A drop cursor is a line that appears when you drag and drop content + * inbetween nodes. + * @see https://tiptap.dev/api/extensions/dropcursor + */ +export const Dropcursor = Extension.create({ + name: 'dropcursor', + + addOptions() { + return { + color: 'currentColor', + width: 1, + class: undefined, + }; + }, + + addProseMirrorPlugins() { + return [dropCursor(this.options)]; + }, +}); diff --git a/lib/block-editor/extensions/Dropcursor/index.ts b/lib/block-editor/extensions/Dropcursor/index.ts new file mode 100644 index 00000000..a17aba6a --- /dev/null +++ b/lib/block-editor/extensions/Dropcursor/index.ts @@ -0,0 +1 @@ +export * from './Dropcursor'; diff --git a/lib/block-editor/extensions/Dropcursor/pmDropcursorReplacement.ts b/lib/block-editor/extensions/Dropcursor/pmDropcursorReplacement.ts new file mode 100644 index 00000000..659accbd --- /dev/null +++ b/lib/block-editor/extensions/Dropcursor/pmDropcursorReplacement.ts @@ -0,0 +1,225 @@ +// adapted from prosemirror-dropcursor +// https://github.com/ProseMirror/prosemirror-dropcursor + +import { Plugin, type EditorState } from '@tiptap/pm/state'; +import { dropPoint } from '@tiptap/pm/transform'; +import { type EditorView } from '@tiptap/pm/view'; +import { isValidDropPosition } from '~/lib/block-editor/utils'; + +type DropCursorOptions = { + /// The color of the cursor. Defaults to `black`. Use `false` to apply no color and rely only on class. + color?: string | false; + + /// The precise width of the cursor in pixels. Defaults to 1. + width?: number; + + /// A CSS class name to add to the cursor element. + class?: string; +}; + +/// Create a plugin that, when added to a ProseMirror instance, +/// causes a decoration to show up at the drop position when something +/// is dragged over the editor. +/// +/// Nodes may add a `disableDropCursor` property to their spec to +/// control the showing of a drop cursor inside them. This may be a +/// boolean or a function, which will be called with a view and a +/// position, and should return a boolean. +export function dropCursor(options: DropCursorOptions = {}): Plugin { + return new Plugin({ + view(editorView) { + return new DropCursorView(editorView, options); + }, + }); +} + +class DropCursorView { + width: number; + color: string | undefined; + class: string | undefined; + cursorPos: number | null = null; + element: HTMLElement | null = null; + timeout = -1; + handlers: { name: string; handler: (event: Event) => void }[]; + + constructor( + readonly editorView: EditorView, + options: DropCursorOptions, + ) { + this.width = options.width ?? 1; + this.color = options.color === false ? undefined : options.color || 'black'; + this.class = options.class; + + this.handlers = ['dragover', 'dragend', 'drop', 'dragleave'].map((name) => { + const handler = (e: Event) => { + (this as any)[name](e); + }; + editorView.dom.addEventListener(name, handler); + return { name, handler }; + }); + } + + destroy() { + this.handlers.forEach(({ name, handler }) => + this.editorView.dom.removeEventListener(name, handler), + ); + } + + update(editorView: EditorView, prevState: EditorState) { + if (this.cursorPos != null && prevState.doc != editorView.state.doc) { + if (this.cursorPos > editorView.state.doc.content.size) + this.setCursor(null); + else this.updateOverlay(); + } + } + + setCursor(pos: number | null) { + if (pos == this.cursorPos) return; + this.cursorPos = pos; + if (pos == null) { + this.element!.parentNode!.removeChild(this.element!); + this.element = null; + } else { + this.updateOverlay(); + } + } + + updateOverlay() { + const $pos = this.editorView.state.doc.resolve(this.cursorPos!); + let isBlock = !$pos.parent.inlineContent, + rect; + const editorDOM = this.editorView.dom, + editorRect = editorDOM.getBoundingClientRect(); + const scaleX = editorRect.width / editorDOM.offsetWidth, + scaleY = editorRect.height / editorDOM.offsetHeight; + if (isBlock) { + const before = $pos.nodeBefore, + after = $pos.nodeAfter; + if (before || after) { + const node = this.editorView.nodeDOM( + this.cursorPos! - (before ? before.nodeSize : 0), + ); + if (node) { + const nodeRect = (node as HTMLElement).getBoundingClientRect(); + let top = before ? nodeRect.bottom : nodeRect.top; + if (before && after) + top = + (top + + ( + this.editorView.nodeDOM(this.cursorPos!) as HTMLElement + ).getBoundingClientRect().top) / + 2; + const halfWidth = (this.width / 2) * scaleY; + rect = { + left: nodeRect.left, + right: nodeRect.right, + top: top - halfWidth, + bottom: top + halfWidth, + }; + } + } + } + if (!rect) { + const coords = this.editorView.coordsAtPos(this.cursorPos!); + const halfWidth = (this.width / 2) * scaleX; + rect = { + left: coords.left - halfWidth, + right: coords.left + halfWidth, + top: coords.top, + bottom: coords.bottom, + }; + } + + const parent = this.editorView.dom.offsetParent as HTMLElement; + if (!this.element) { + this.element = parent.appendChild(document.createElement('div')); + if (this.class) this.element.className = this.class; + this.element.style.cssText = + 'position: absolute; z-index: 50; pointer-events: none;'; + if (this.color) { + this.element.style.backgroundColor = this.color; + } + } + this.element.classList.toggle('prosemirror-dropcursor-block', isBlock); + this.element.classList.toggle('prosemirror-dropcursor-inline', !isBlock); + let parentLeft, parentTop; + if ( + !parent || + (parent == document.body && getComputedStyle(parent).position == 'static') + ) { + parentLeft = -pageXOffset; + parentTop = -pageYOffset; + } else { + const rect = parent.getBoundingClientRect(); + const parentScaleX = rect.width / parent.offsetWidth, + parentScaleY = rect.height / parent.offsetHeight; + parentLeft = rect.left - parent.scrollLeft * parentScaleX; + parentTop = rect.top - parent.scrollTop * parentScaleY; + } + this.element.style.left = (rect.left - parentLeft) / scaleX + 'px'; + this.element.style.top = (rect.top - parentTop) / scaleY + 'px'; + this.element.style.width = (rect.right - rect.left) / scaleX + 'px'; + this.element.style.height = (rect.bottom - rect.top) / scaleY + 'px'; + } + + scheduleRemoval(timeout: number) { + clearTimeout(this.timeout); + this.timeout = setTimeout(() => this.setCursor(null), timeout); + } + + dragover(event: DragEvent) { + if (!this.editorView.editable) return; + const pos = this.editorView.posAtCoords({ + left: event.clientX, + top: event.clientY, + }); + + const node = + pos && pos.inside >= 0 && this.editorView.state.doc.nodeAt(pos.inside); + const disableDropCursor = node && node.type.spec.disableDropCursor; + const disabled = + typeof disableDropCursor == 'function' + ? disableDropCursor(this.editorView, pos, event) + : disableDropCursor; + + // if variable is being dragged to an invalid position, prevent dropcursor from being displayed + if (event.dataTransfer) { + const isSidePanelDrop = event.dataTransfer.types.includes( + 'application/x-content-type', + ); + if (isSidePanelDrop && !isValidDropPosition(this.editorView, event)) { + return; + } + } + + if (pos && !disabled) { + let target: number | null = pos.pos; + if (this.editorView.dragging && this.editorView.dragging.slice) { + const point = dropPoint( + this.editorView.state.doc, + target, + this.editorView.dragging.slice, + ); + if (point != null) target = point; + } + this.setCursor(target); + this.scheduleRemoval(5000); + } + } + + dragend() { + this.scheduleRemoval(20); + } + + drop() { + this.scheduleRemoval(20); + } + + dragleave(event: DragEvent) { + if ( + event.target == this.editorView.dom || + !this.editorView.dom.contains((event as any).relatedTarget) + ) + this.setCursor(null); + } +} diff --git a/lib/block-editor/extensions/Group/Group.tsx b/lib/block-editor/extensions/Group/Group.tsx new file mode 100644 index 00000000..0be4e924 --- /dev/null +++ b/lib/block-editor/extensions/Group/Group.tsx @@ -0,0 +1,46 @@ +import { Node, mergeAttributes } from '@tiptap/react'; + +const gridClasses: Record = { + 1: 'grid-cols-1', + 2: 'grid-cols-2', + 3: 'grid-cols-3', + 4: 'grid-cols-4', +}; + +export const Group = Node.create({ + name: 'group', + + group: 'block', + + content: 'block*', + + parseHTML() { + return [{ tag: 'div[data-type="group"]' }]; + }, + + renderHTML({ HTMLAttributes }) { + const { columns } = HTMLAttributes as { columns: number }; + + const gridClass = gridClasses[columns]; + + return [ + 'div', + mergeAttributes(HTMLAttributes, { + 'data-type': 'group', + 'class': `gap-4 p-4 grid rounded-small ${gridClass} border hover:border-accent`, + }), + 0, + ]; + }, + + addAttributes() { + return { + columns: { + default: 2, + }, + groupRequired: { + default: false, + }, + }; + }, +}); diff --git a/lib/block-editor/extensions/Group/GroupMenu.tsx b/lib/block-editor/extensions/Group/GroupMenu.tsx new file mode 100644 index 00000000..4aa4502b --- /dev/null +++ b/lib/block-editor/extensions/Group/GroupMenu.tsx @@ -0,0 +1,148 @@ +import { useEditorState, type Editor } from '@tiptap/react'; +import { ChevronDown, CircleAlert, Trash } from 'lucide-react'; +import { useCallback, useRef } from 'react'; +import { sticky } from 'tippy.js'; +import DropdownMenu from '~/components/DropdownMenu'; +import Toolbar from '~/components/Toolbar'; +import BubbleMenu from '~/components/block-editor/BubbleMenu'; +import { getRenderContainer } from '../../utils'; +import { toggleGroupRequired } from './utils'; + +type GroupEditorState = { + columns: number; + groupRequired: boolean; + hovered: boolean; +}; + +export default function GroupMenu({ + editor, + appendTo, +}: { + editor: Editor | null; + appendTo: React.RefObject; +}) { + const shouldShow = useCallback(() => { + const isGroup = editor?.isActive('group'); + return !!isGroup; + }, [editor]); + + const getReferenceClientRect = useCallback(() => { + if (!editor) return new DOMRect(-1000, -1000, 0, 0); + const renderContainer = getRenderContainer(editor, 'group'); + + const rect = + renderContainer?.getBoundingClientRect() ?? + new DOMRect(-1000, -1000, 0, 0); + + return rect; + }, [editor]); + + const { columns, groupRequired } = useEditorState({ + editor, + selector: (ctx) => { + return { + columns: ctx.editor?.getAttributes('group')?.columns as number, + groupRequired: ctx.editor?.getAttributes('group') + ?.groupRequired as boolean, + }; + }, + }) as GroupEditorState; + + const containerRef = useRef(null); + + if (!editor) { + return null; + } + + return ( + appendTo?.current ?? document.body, + sticky: true, + placement: 'bottom', + }} + > + + { + editor?.commands.deleteNode('group'); + }} + > + + + + + + Select number of columns + + editor?.commands.updateAttributes('group', { columns: 1 }) + } + > + 1 + + + editor?.commands.updateAttributes('group', { columns: 2 }) + } + > + 2 + + + editor?.commands.updateAttributes('group', { columns: 3 }) + } + > + 3 + + + editor?.commands.updateAttributes('group', { columns: 4 }) + } + > + 4 + + + + + + + {columns} Columns + + + + + + {/* required toggle */} + toggleGroupRequired(editor)} + > + + Group Required + + + + + ); +} diff --git a/lib/block-editor/extensions/Group/index.ts b/lib/block-editor/extensions/Group/index.ts new file mode 100644 index 00000000..8401278d --- /dev/null +++ b/lib/block-editor/extensions/Group/index.ts @@ -0,0 +1 @@ +export * from './Group'; diff --git a/lib/block-editor/extensions/Group/utils.ts b/lib/block-editor/extensions/Group/utils.ts new file mode 100644 index 00000000..2e9872f3 --- /dev/null +++ b/lib/block-editor/extensions/Group/utils.ts @@ -0,0 +1,49 @@ +import { type Node } from '@tiptap/pm/model'; +import type { Editor } from '@tiptap/react'; + +type GroupNode = Node & { attrs: { groupRequired: boolean } }; + +export function toggleGroupRequired(editor: Editor) { + if (!editor) return; + + // find the current group node + const { from, to } = editor.state.selection; + let groupPos: number | null = null; + let groupNode: GroupNode | null = null; + + editor.state.doc.nodesBetween(from, to, (node, pos) => { + if (node.type.name === 'group') { + groupPos = pos; + if ('groupRequired' in node.attrs) { + groupNode = node as GroupNode; + } + return false; + } + return true; + }); + + if (!groupNode || groupPos === null) return; + + const groupRequired = (groupNode as GroupNode).attrs.groupRequired; + const groupNodeSize = (groupNode as GroupNode).nodeSize; + + // traverse nodes inside the group node and update the required attribute of variable nodes + editor.state.doc.nodesBetween( + groupPos, + groupPos + groupNodeSize, + (node, pos) => { + if (node.type.name === 'variable') { + editor.view.dispatch( + editor.state.tr.setNodeMarkup(pos, undefined, { + ...node.attrs, + required: !groupRequired, + }), + ); + } + return true; + }, + ); + + // update the group's groupRequired attribute + editor.commands.updateAttributes('group', { groupRequired: !groupRequired }); +} diff --git a/lib/block-editor/extensions/HorizontalRule/HorizontalRule.tsx b/lib/block-editor/extensions/HorizontalRule/HorizontalRule.tsx new file mode 100644 index 00000000..5e7b685a --- /dev/null +++ b/lib/block-editor/extensions/HorizontalRule/HorizontalRule.tsx @@ -0,0 +1,17 @@ +import TiptapHorizontalRule from '@tiptap/extension-horizontal-rule'; +import { NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap/react'; +import Divider from '~/components/layout/Divider'; + +const WrappedDivider = () => ( + + + +); + +export const HorizontalRule = TiptapHorizontalRule.extend({ + addNodeView() { + return ReactNodeViewRenderer(WrappedDivider); + }, +}); + +export default HorizontalRule; diff --git a/lib/block-editor/extensions/HorizontalRule/index.ts b/lib/block-editor/extensions/HorizontalRule/index.ts new file mode 100644 index 00000000..65fc6ace --- /dev/null +++ b/lib/block-editor/extensions/HorizontalRule/index.ts @@ -0,0 +1 @@ +export * from './HorizontalRule' diff --git a/lib/block-editor/extensions/Image/Image.ts b/lib/block-editor/extensions/Image/Image.ts new file mode 100644 index 00000000..cd33a575 --- /dev/null +++ b/lib/block-editor/extensions/Image/Image.ts @@ -0,0 +1,7 @@ +import { Image as BaseImage } from '@tiptap/extension-image' + +export const Image = BaseImage.extend({ + group: 'block', +}) + +export default Image diff --git a/lib/block-editor/extensions/Image/index.ts b/lib/block-editor/extensions/Image/index.ts new file mode 100644 index 00000000..072b1619 --- /dev/null +++ b/lib/block-editor/extensions/Image/index.ts @@ -0,0 +1 @@ +export * from './Image' diff --git a/lib/block-editor/extensions/ImageBlock/ImageBlock.ts b/lib/block-editor/extensions/ImageBlock/ImageBlock.ts new file mode 100644 index 00000000..ffd88e61 --- /dev/null +++ b/lib/block-editor/extensions/ImageBlock/ImageBlock.ts @@ -0,0 +1,117 @@ +import { mergeAttributes, type Range } from '@tiptap/core'; +import { ReactNodeViewRenderer } from '@tiptap/react'; + +import { Image } from '../Image'; +import { ImageBlockView } from './components/ImageBlockView'; + +declare module '@tiptap/core' { + type Commands = { + imageBlock: { + setImageBlock: (attributes: { src: string }) => ReturnType; + setImageBlockAt: (attributes: { + src: string; + pos: number | Range; + }) => ReturnType; + setImageBlockAlign: (align: 'left' | 'center' | 'right') => ReturnType; + setImageBlockWidth: (width: number) => ReturnType; + }; + }; +} + +export const ImageBlock = Image.extend({ + name: 'imageBlock', + + group: 'block', + + defining: true, + + isolating: true, + + addAttributes() { + return { + src: { + default: '', + parseHTML: (element) => element.getAttribute('src'), + renderHTML: (attributes) => ({ + src: attributes.src, + }), + }, + width: { + default: '100%', + parseHTML: (element) => element.getAttribute('data-width'), + renderHTML: (attributes) => ({ + 'data-width': attributes.width, + }), + }, + align: { + default: 'center', + parseHTML: (element) => element.getAttribute('data-align'), + renderHTML: (attributes) => ({ + 'data-align': attributes.align, + }), + }, + alt: { + default: undefined, + parseHTML: (element) => element.getAttribute('alt'), + renderHTML: (attributes) => ({ + alt: attributes.alt, + }), + }, + }; + }, + + parseHTML() { + return [ + { + tag: 'img[src*="tiptap.dev"]:not([src^="data:"]), img[src*="windows.net"]:not([src^="data:"])', + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + 'img', + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), + ]; + }, + + addCommands() { + return { + setImageBlock: + (attrs) => + ({ commands }) => { + return commands.insertContent({ + type: 'imageBlock', + attrs: { src: attrs.src }, + }); + }, + + setImageBlockAt: + (attrs) => + ({ commands }) => { + return commands.insertContentAt(attrs.pos, { + type: 'imageBlock', + attrs: { src: attrs.src }, + }); + }, + + setImageBlockAlign: + (align) => + ({ commands }) => + commands.updateAttributes('imageBlock', { align }), + + setImageBlockWidth: + (width) => + ({ commands }) => + commands.updateAttributes('imageBlock', { + width: `${Math.max(0, Math.min(100, width))}%`, + }), + }; + }, + + addNodeView() { + return ReactNodeViewRenderer(ImageBlockView); + }, +}); + +export default ImageBlock; diff --git a/lib/block-editor/extensions/ImageBlock/components/ImageBlockMenu.tsx b/lib/block-editor/extensions/ImageBlock/components/ImageBlockMenu.tsx new file mode 100644 index 00000000..719a3bbf --- /dev/null +++ b/lib/block-editor/extensions/ImageBlock/components/ImageBlockMenu.tsx @@ -0,0 +1,98 @@ +import { BubbleMenu as BaseBubbleMenu, useEditorState } from '@tiptap/react' +import React, { useCallback, useRef } from 'react' +import { Instance, sticky } from 'tippy.js' +import { v4 as uuid } from 'uuid' + +import { Toolbar } from '@/components/ui/Toolbar' +import { Icon } from '@/components/ui/Icon' +import { ImageBlockWidth } from './ImageBlockWidth' +import { MenuProps } from '@/components/menus/types' +import { getRenderContainer } from '@/lib/utils' + +export const ImageBlockMenu = ({ editor, appendTo }: MenuProps): JSX.Element => { + const menuRef = useRef(null) + const tippyInstance = useRef(null) + + const getReferenceClientRect = useCallback(() => { + const renderContainer = getRenderContainer(editor, 'node-imageBlock') + const rect = renderContainer?.getBoundingClientRect() || new DOMRect(-1000, -1000, 0, 0) + + return rect + }, [editor]) + + const shouldShow = useCallback(() => { + const isActive = editor.isActive('imageBlock') + + return isActive + }, [editor]) + + const onAlignImageLeft = useCallback(() => { + editor.chain().focus(undefined, { scrollIntoView: false }).setImageBlockAlign('left').run() + }, [editor]) + + const onAlignImageCenter = useCallback(() => { + editor.chain().focus(undefined, { scrollIntoView: false }).setImageBlockAlign('center').run() + }, [editor]) + + const onAlignImageRight = useCallback(() => { + editor.chain().focus(undefined, { scrollIntoView: false }).setImageBlockAlign('right').run() + }, [editor]) + + const onWidthChange = useCallback( + (value: number) => { + editor.chain().focus(undefined, { scrollIntoView: false }).setImageBlockWidth(value).run() + }, + [editor], + ) + const { isImageCenter, isImageLeft, isImageRight, width } = useEditorState({ + editor, + selector: ctx => { + return { + isImageLeft: ctx.editor.isActive('imageBlock', { align: 'left' }), + isImageCenter: ctx.editor.isActive('imageBlock', { align: 'center' }), + isImageRight: ctx.editor.isActive('imageBlock', { align: 'right' }), + width: parseInt(ctx.editor.getAttributes('imageBlock')?.width || 0), + } + }, + }) + + return ( + { + tippyInstance.current = instance + }, + appendTo: () => { + return appendTo?.current + }, + plugins: [sticky], + sticky: 'popper', + }} + > + + + + + + + + + + + + + + + ) +} + +export default ImageBlockMenu diff --git a/lib/block-editor/extensions/ImageBlock/components/ImageBlockView.tsx b/lib/block-editor/extensions/ImageBlock/components/ImageBlockView.tsx new file mode 100644 index 00000000..5c762cb6 --- /dev/null +++ b/lib/block-editor/extensions/ImageBlock/components/ImageBlockView.tsx @@ -0,0 +1,45 @@ +import { type Node } from '@tiptap/pm/model'; +import { type Editor, NodeViewWrapper } from '@tiptap/react'; +import { useCallback, useRef } from 'react'; +import { cn } from '~/lib/utils'; + +type ImageBlockViewProps = { + editor: Editor; + getPos: () => number; + node: Node; + updateAttributes: (attrs: Record) => void; +}; + +export const ImageBlockView = (props: ImageBlockViewProps) => { + const { editor, getPos, node } = props as ImageBlockViewProps & { + node: Node & { + attrs: { + src: string; + }; + }; + }; + const imageWrapperRef = useRef(null); + const { src } = node.attrs; + + const wrapperClassName = cn( + node.attrs.align === 'left' ? 'ml-0' : 'ml-auto', + node.attrs.align === 'right' ? 'mr-0' : 'mr-auto', + node.attrs.align === 'center' && 'mx-auto', + ); + + const onClick = useCallback(() => { + editor.commands.setNodeSelection(getPos()); + }, [getPos, editor.commands]); + + return ( + + + + + + + + ); +}; + +export default ImageBlockView; diff --git a/lib/block-editor/extensions/ImageBlock/components/ImageBlockWidth.tsx b/lib/block-editor/extensions/ImageBlock/components/ImageBlockWidth.tsx new file mode 100644 index 00000000..5edea3a9 --- /dev/null +++ b/lib/block-editor/extensions/ImageBlock/components/ImageBlockWidth.tsx @@ -0,0 +1,40 @@ +import { memo, useCallback, useEffect, useState } from 'react' + +export type ImageBlockWidthProps = { + onChange: (value: number) => void + value: number +} + +export const ImageBlockWidth = memo(({ onChange, value }: ImageBlockWidthProps) => { + const [currentValue, setCurrentValue] = useState(value) + + useEffect(() => { + setCurrentValue(value) + }, [value]) + + const handleChange = useCallback( + (e: React.ChangeEvent) => { + const nextValue = parseInt(e.target.value) + onChange(nextValue) + setCurrentValue(nextValue) + }, + [onChange], + ) + + return ( + + + {value}% + + ) +}) + +ImageBlockWidth.displayName = 'ImageBlockWidth' diff --git a/lib/block-editor/extensions/ImageBlock/index.ts b/lib/block-editor/extensions/ImageBlock/index.ts new file mode 100644 index 00000000..e870ec86 --- /dev/null +++ b/lib/block-editor/extensions/ImageBlock/index.ts @@ -0,0 +1 @@ +export * from './ImageBlock' diff --git a/lib/block-editor/extensions/Link/Link.ts b/lib/block-editor/extensions/Link/Link.ts new file mode 100644 index 00000000..defaae76 --- /dev/null +++ b/lib/block-editor/extensions/Link/Link.ts @@ -0,0 +1,49 @@ +import { mergeAttributes } from '@tiptap/core'; +import TiptapLink from '@tiptap/extension-link'; +import { Plugin } from '@tiptap/pm/state'; +import { type EditorView } from '@tiptap/pm/view'; + +export const Link = TiptapLink.extend({ + inclusive: false, + + parseHTML() { + return [ + { + tag: 'a[href]:not([data-type="button"]):not([href *= "javascript:" i])', + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + 'a', + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { + class: 'text-link hover:underline', + }), + 0, + ]; + }, + + addProseMirrorPlugins() { + const { editor } = this; + + return [ + ...(this.parent?.() ?? []), + new Plugin({ + props: { + handleKeyDown: (view: EditorView, event: KeyboardEvent) => { + const { selection } = editor.state; + + if (event.key === 'Escape' && selection.empty !== true) { + editor.commands.focus(selection.to, { scrollIntoView: false }); + } + + return false; + }, + }, + }), + ]; + }, +}); + +export default Link; diff --git a/lib/block-editor/extensions/Link/index.ts b/lib/block-editor/extensions/Link/index.ts new file mode 100644 index 00000000..9378deb1 --- /dev/null +++ b/lib/block-editor/extensions/Link/index.ts @@ -0,0 +1 @@ +export * from './Link' diff --git a/lib/block-editor/extensions/MultiColumn/Column.ts b/lib/block-editor/extensions/MultiColumn/Column.ts new file mode 100644 index 00000000..3ae4203b --- /dev/null +++ b/lib/block-editor/extensions/MultiColumn/Column.ts @@ -0,0 +1,37 @@ +import { Node, mergeAttributes } from '@tiptap/core'; + +export const Column = Node.create({ + name: 'column', + + content: 'block+', + + isolating: true, + + addAttributes() { + return { + position: { + default: '', + parseHTML: (element) => element.getAttribute('data-position'), + renderHTML: (attributes) => ({ 'data-position': attributes.position }), + }, + }; + }, + + renderHTML({ HTMLAttributes }) { + return [ + 'div', + mergeAttributes(HTMLAttributes, { 'data-type': 'column' }), + 0, + ]; + }, + + parseHTML() { + return [ + { + tag: 'div[data-type="column"]', + }, + ]; + }, +}); + +export default Column; diff --git a/lib/block-editor/extensions/MultiColumn/Columns.ts b/lib/block-editor/extensions/MultiColumn/Columns.ts new file mode 100644 index 00000000..817596dd --- /dev/null +++ b/lib/block-editor/extensions/MultiColumn/Columns.ts @@ -0,0 +1,65 @@ +import { Node } from '@tiptap/core' + +export enum ColumnLayout { + SidebarLeft = 'sidebar-left', + SidebarRight = 'sidebar-right', + TwoColumn = 'two-column', +} + +declare module '@tiptap/core' { + interface Commands { + columns: { + setColumns: () => ReturnType + setLayout: (layout: ColumnLayout) => ReturnType + } + } +} + +export const Columns = Node.create({ + name: 'columns', + + group: 'columns', + + content: 'column column', + + defining: true, + + isolating: true, + + addAttributes() { + return { + layout: { + default: ColumnLayout.TwoColumn, + }, + } + }, + + addCommands() { + return { + setColumns: + () => + ({ commands }) => + commands.insertContent( + ``, + ), + setLayout: + (layout: ColumnLayout) => + ({ commands }) => + commands.updateAttributes('columns', { layout }), + } + }, + + renderHTML({ HTMLAttributes }) { + return ['div', { 'data-type': 'columns', class: `layout-${HTMLAttributes.layout}` }, 0] + }, + + parseHTML() { + return [ + { + tag: 'div[data-type="columns"]', + }, + ] + }, +}) + +export default Columns diff --git a/lib/block-editor/extensions/MultiColumn/index.ts b/lib/block-editor/extensions/MultiColumn/index.ts new file mode 100644 index 00000000..8537f09a --- /dev/null +++ b/lib/block-editor/extensions/MultiColumn/index.ts @@ -0,0 +1,2 @@ +export * from './Columns' +export * from './Column' diff --git a/lib/block-editor/extensions/MultiColumn/menus/ColumnsMenu.tsx b/lib/block-editor/extensions/MultiColumn/menus/ColumnsMenu.tsx new file mode 100644 index 00000000..ca6d390f --- /dev/null +++ b/lib/block-editor/extensions/MultiColumn/menus/ColumnsMenu.tsx @@ -0,0 +1,79 @@ +import { BubbleMenu as BaseBubbleMenu, useEditorState } from '@tiptap/react' +import { useCallback } from 'react' +import { sticky } from 'tippy.js' +import { v4 as uuid } from 'uuid' + +import { MenuProps } from '@/components/menus/types' +import { getRenderContainer } from '@/lib/utils/getRenderContainer' +import { Toolbar } from '@/components/ui/Toolbar' +import { ColumnLayout } from '../Columns' +import { Icon } from '@/components/ui/Icon' + +export const ColumnsMenu = ({ editor, appendTo }: MenuProps) => { + const getReferenceClientRect = useCallback(() => { + const renderContainer = getRenderContainer(editor, 'columns') + const rect = renderContainer?.getBoundingClientRect() || new DOMRect(-1000, -1000, 0, 0) + + return rect + }, [editor]) + + const shouldShow = useCallback(() => { + const isColumns = editor.isActive('columns') + return isColumns + }, [editor]) + + const onColumnLeft = useCallback(() => { + editor.chain().focus().setLayout(ColumnLayout.SidebarLeft).run() + }, [editor]) + + const onColumnRight = useCallback(() => { + editor.chain().focus().setLayout(ColumnLayout.SidebarRight).run() + }, [editor]) + + const onColumnTwo = useCallback(() => { + editor.chain().focus().setLayout(ColumnLayout.TwoColumn).run() + }, [editor]) + const { isColumnLeft, isColumnRight, isColumnTwo } = useEditorState({ + editor, + selector: ctx => { + return { + isColumnLeft: ctx.editor.isActive('columns', { layout: ColumnLayout.SidebarLeft }), + isColumnRight: ctx.editor.isActive('columns', { layout: ColumnLayout.SidebarRight }), + isColumnTwo: ctx.editor.isActive('columns', { layout: ColumnLayout.TwoColumn }), + } + }, + }) + + return ( + appendTo?.current, + plugins: [sticky], + sticky: 'popper', + }} + > + + + + + + + + + + + + + ) +} + +export default ColumnsMenu diff --git a/lib/block-editor/extensions/MultiColumn/menus/index.ts b/lib/block-editor/extensions/MultiColumn/menus/index.ts new file mode 100644 index 00000000..5a7b3232 --- /dev/null +++ b/lib/block-editor/extensions/MultiColumn/menus/index.ts @@ -0,0 +1 @@ +export * from './ColumnsMenu' diff --git a/lib/block-editor/extensions/NodeBubbleMenu/NodeBubbleMenu.tsx b/lib/block-editor/extensions/NodeBubbleMenu/NodeBubbleMenu.tsx new file mode 100644 index 00000000..3cfbf65b --- /dev/null +++ b/lib/block-editor/extensions/NodeBubbleMenu/NodeBubbleMenu.tsx @@ -0,0 +1,132 @@ +import { useEditorState, type Editor } from '@tiptap/react'; +import { Bold, Italic, Link, Trash } from 'lucide-react'; +import { useCallback } from 'react'; +import BubbleMenu from '~/components/block-editor/BubbleMenu'; +import { Button } from '~/components/Button'; +import Popover from '~/components/Popover'; +import Toolbar from '~/components/Toolbar'; +import VariableMenu from '../Variable/VariableMenu'; + +export const NodeBubbleMenu = ({ editor }: { editor: Editor | null }) => { + const shouldShow = useCallback(() => { + if (!editor) return false; + const isVariable = editor.isActive('variable'); + + if (isVariable) return true; + + if (editor.view.state.selection.empty) return false; + + const isText = editor.isActive('paragraph') || editor.isActive('heading'); + + return !!isVariable || !!isText; + }, [editor]); + + const editorState = useEditorState({ + editor, + selector: ({ editor }) => { + return { + isBold: !!editor?.isActive('bold'), + isItalic: !!editor?.isActive('italic'), + isLink: !!editor?.isActive('link'), + activeLink: editor?.getAttributes('link').href as string | null, + isVariable: + !!editor?.isActive('variable') || + !!editor?.isActive('control') || + !!editor?.isActive('label') || + !!editor?.isActive('hint'), + isGroup: !!editor?.isActive('group'), + }; + }, + }); + + if (!editor) { + return null; + } + + const handleLinkSubmit = (event: React.FormEvent) => { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + const url = formData.get('url') as string; + + if (url) { + editor + .chain() + .focus() + .setLink({ + href: url, + target: '_blank', + }) + .run(); + } + }; + + const handleLinkRemove = () => { + editor?.chain().focus().unsetLink().run(); + }; + + return ( + + + + { + editor?.chain().focus().toggleBold().run(); + }} + active={editorState?.isBold} + > + + + { + editor?.chain().focus().toggleItalic().run(); + }} + active={editorState?.isItalic} + > + + + + + + + {editorState.activeLink} + + + + + + ) : ( + + + + Set Link + + + ) + } + > + + + + + {editorState?.isVariable && } + + + ); +}; diff --git a/lib/block-editor/extensions/NodeBubbleMenu/index.ts b/lib/block-editor/extensions/NodeBubbleMenu/index.ts new file mode 100644 index 00000000..62083573 --- /dev/null +++ b/lib/block-editor/extensions/NodeBubbleMenu/index.ts @@ -0,0 +1 @@ +export * from './NodeBubbleMenu'; diff --git a/lib/block-editor/extensions/Selection/Selection.ts b/lib/block-editor/extensions/Selection/Selection.ts new file mode 100644 index 00000000..19c7b529 --- /dev/null +++ b/lib/block-editor/extensions/Selection/Selection.ts @@ -0,0 +1,36 @@ +import { Extension } from '@tiptap/core' +import { Plugin, PluginKey } from '@tiptap/pm/state' +import { Decoration, DecorationSet } from '@tiptap/pm/view' + +export const Selection = Extension.create({ + name: 'selection', + + addProseMirrorPlugins() { + const { editor } = this + + return [ + new Plugin({ + key: new PluginKey('selection'), + props: { + decorations(state) { + if (state.selection.empty) { + return null + } + + if (editor.isFocused === true) { + return null + } + + return DecorationSet.create(state.doc, [ + Decoration.inline(state.selection.from, state.selection.to, { + class: 'selection', + }), + ]) + }, + }, + }), + ] + }, +}) + +export default Selection diff --git a/lib/block-editor/extensions/Selection/index.ts b/lib/block-editor/extensions/Selection/index.ts new file mode 100644 index 00000000..9279d55f --- /dev/null +++ b/lib/block-editor/extensions/Selection/index.ts @@ -0,0 +1 @@ +export * from './Selection' diff --git a/lib/block-editor/extensions/SlashCommand/CommandButton.tsx b/lib/block-editor/extensions/SlashCommand/CommandButton.tsx new file mode 100644 index 00000000..5a969b01 --- /dev/null +++ b/lib/block-editor/extensions/SlashCommand/CommandButton.tsx @@ -0,0 +1,33 @@ +import { forwardRef } from 'react' +import { cn } from '@/lib/utils' +import { icons } from 'lucide-react' +import { Icon } from '@/components/ui/Icon' + +export type CommandButtonProps = { + active?: boolean + description: string + icon: keyof typeof icons + onClick: () => void + title: string +} + +export const CommandButton = forwardRef( + ({ active, icon, onClick, title }, ref) => { + const wrapperClass = cn( + 'flex text-neutral-500 items-center text-xs font-semibold justify-start p-1.5 gap-2 rounded', + !active && 'bg-transparent hover:bg-neutral-50 hover:text-black', + active && 'bg-neutral-100 text-black hover:bg-neutral-100', + ) + + return ( + + + + {title} + + + ) + }, +) + +CommandButton.displayName = 'CommandButton' diff --git a/lib/block-editor/extensions/SlashCommand/MenuList.tsx b/lib/block-editor/extensions/SlashCommand/MenuList.tsx new file mode 100644 index 00000000..14c7b35f --- /dev/null +++ b/lib/block-editor/extensions/SlashCommand/MenuList.tsx @@ -0,0 +1,162 @@ +import { CircleIcon } from 'lucide-react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { Button } from '~/components/Button'; +import Surface from '~/components/layout/Surface'; +import { type Command, type MenuListProps } from './types'; + +export const MenuList = React.forwardRef((props: MenuListProps, ref) => { + const scrollContainer = useRef(null); + const activeItem = useRef(null); + const [selectedGroupIndex, setSelectedGroupIndex] = useState(0); + const [selectedCommandIndex, setSelectedCommandIndex] = useState(0); + + // Anytime the groups change, i.e. the user types to narrow it down, we want to + // reset the current selection to the first menu item + useEffect(() => { + setSelectedGroupIndex(0); + setSelectedCommandIndex(0); + }, [props.items]); + + const selectItem = useCallback( + (groupIndex: number, commandIndex: number) => { + const command = props.items[groupIndex].commands[commandIndex]; + props.command(command); + }, + [props], + ); + + React.useImperativeHandle(ref, () => ({ + onKeyDown: ({ event }: { event: React.KeyboardEvent }) => { + if (event.key === 'ArrowDown') { + if (!props.items.length) { + return false; + } + + const commands = props.items[selectedGroupIndex].commands; + + let newCommandIndex = selectedCommandIndex + 1; + let newGroupIndex = selectedGroupIndex; + + if (commands.length - 1 < newCommandIndex) { + newCommandIndex = 0; + newGroupIndex = selectedGroupIndex + 1; + } + + if (props.items.length - 1 < newGroupIndex) { + newGroupIndex = 0; + } + + setSelectedCommandIndex(newCommandIndex); + setSelectedGroupIndex(newGroupIndex); + + return true; + } + + if (event.key === 'ArrowUp') { + if (!props.items.length) { + return false; + } + + let newCommandIndex = selectedCommandIndex - 1; + let newGroupIndex = selectedGroupIndex; + + if (newCommandIndex < 0) { + newGroupIndex = selectedGroupIndex - 1; + newCommandIndex = + props.items[newGroupIndex]?.commands.length - 1 || 0; + } + + if (newGroupIndex < 0) { + newGroupIndex = props.items.length - 1; + newCommandIndex = props.items[newGroupIndex].commands.length - 1; + } + + setSelectedCommandIndex(newCommandIndex); + setSelectedGroupIndex(newGroupIndex); + + return true; + } + + if (event.key === 'Enter') { + if ( + !props.items.length || + selectedGroupIndex === -1 || + selectedCommandIndex === -1 + ) { + return false; + } + + selectItem(selectedGroupIndex, selectedCommandIndex); + + return true; + } + + return false; + }, + })); + + useEffect(() => { + if (activeItem.current && scrollContainer.current) { + const offsetTop = activeItem.current.offsetTop; + const offsetHeight = activeItem.current.offsetHeight; + + scrollContainer.current.scrollTop = offsetTop - offsetHeight; + } + }, [selectedCommandIndex, selectedGroupIndex]); + + const createCommandClickHandler = useCallback( + (groupIndex: number, commandIndex: number) => { + return () => { + selectItem(groupIndex, commandIndex); + }; + }, + [selectItem], + ); + + if (!props.items.length) { + return null; + } + + return ( + + + {props.items.map((group, groupIndex: number) => ( + + + {group.title} + + {group.commands.map((command: Command, commandIndex: number) => ( + + + {command.label} + + ))} + + ))} + + + ); +}); + +MenuList.displayName = 'MenuList'; + +export default MenuList; diff --git a/lib/block-editor/extensions/SlashCommand/SlashCommand.ts b/lib/block-editor/extensions/SlashCommand/SlashCommand.ts new file mode 100644 index 00000000..1bed4000 --- /dev/null +++ b/lib/block-editor/extensions/SlashCommand/SlashCommand.ts @@ -0,0 +1,287 @@ +import { type Editor, Extension } from '@tiptap/core'; +import { PluginKey } from '@tiptap/pm/state'; +import { ReactRenderer } from '@tiptap/react'; +import Suggestion, { + type SuggestionKeyDownProps, + type SuggestionProps, +} from '@tiptap/suggestion'; +import tippy from 'tippy.js'; + +import { GROUPS } from './groups'; +import { MenuList } from './MenuList'; + +const extensionName = 'slashCommand'; + +let popup: any; + +export const SlashCommand = Extension.create({ + name: extensionName, + + priority: 200, + + onCreate() { + popup = tippy('body', { + interactive: true, + trigger: 'manual', + placement: 'bottom-start', + theme: 'slash-command', + maxWidth: '16rem', + offset: [16, 8], + popperOptions: { + strategy: 'fixed', + modifiers: [ + { + name: 'flip', + enabled: false, + }, + ], + }, + }); + }, + + addProseMirrorPlugins() { + return [ + Suggestion({ + editor: this.editor, + char: '/', + allowSpaces: true, + startOfLine: true, + pluginKey: new PluginKey(extensionName), + allow: ({ state, range }) => { + const $from = state.doc.resolve(range.from); + const isRootDepth = $from.depth === 1; + const isParagraph = $from.parent.type.name === 'paragraph'; + const isStartOfNode = $from.parent.textContent?.startsWith('/'); + // TODO + const isInColumn = this.editor.isActive('column'); + + const afterContent = $from.parent.textContent?.substring( + $from.parent.textContent?.indexOf('/'), + ); + const isValidAfterContent = !afterContent?.endsWith(' '); + + return ( + ((isRootDepth && isParagraph && isStartOfNode) || + (isInColumn && isParagraph && isStartOfNode)) && + isValidAfterContent + ); + }, + command: ({ editor, props }: { editor: Editor; props: any }) => { + const { view, state } = editor; + const { $head, $from } = view.state.selection; + + const end = $from.pos; + const from = $head?.nodeBefore + ? end - + ($head.nodeBefore.text?.substring( + $head.nodeBefore.text?.indexOf('/'), + ).length ?? 0) + : $from.start(); + + const tr = state.tr.deleteRange(from, end); + view.dispatch(tr); + + props.action(editor); + view.focus(); + }, + items: ({ query }: { query: string }) => { + const withFilteredCommands = GROUPS.map((group) => ({ + ...group, + commands: group.commands + .filter((item) => { + const labelNormalized = item.label.toLowerCase().trim(); + const queryNormalized = query.toLowerCase().trim(); + + if (item.aliases) { + const aliases = item.aliases.map((alias) => + alias.toLowerCase().trim(), + ); + + return ( + labelNormalized.includes(queryNormalized) || + aliases.includes(queryNormalized) + ); + } + + return labelNormalized.includes(queryNormalized); + }) + .filter((command) => + command.shouldBeHidden + ? !command.shouldBeHidden(this.editor) + : true, + ), + })); + + const withoutEmptyGroups = withFilteredCommands.filter((group) => { + if (group.commands.length > 0) { + return true; + } + + return false; + }); + + const withEnabledSettings = withoutEmptyGroups.map((group) => ({ + ...group, + commands: group.commands.map((command) => ({ + ...command, + isEnabled: true, + })), + })); + + return withEnabledSettings; + }, + render: () => { + let component: any; + + let scrollHandler: (() => void) | null = null; + + return { + onStart: (props: SuggestionProps) => { + component = new ReactRenderer(MenuList, { + props, + editor: props.editor, + }); + + const { view } = props.editor; + + const editorNode = view.dom; + + const getReferenceClientRect = () => { + if (!props.clientRect) { + return props.editor.storage[extensionName].rect; + } + + const rect = props.clientRect(); + + if (!rect) { + return props.editor.storage[extensionName].rect; + } + + let yPos = rect.y; + + if ( + rect.top + component.element.offsetHeight + 40 > + window.innerHeight + ) { + const diff = + rect.top + + component.element.offsetHeight - + window.innerHeight + + 40; + yPos = rect.y - diff; + } + + // Account for when the editor is bound inside a container that doesn't go all the way to the edge of the screen + const editorXOffset = editorNode.getBoundingClientRect().x; + return new DOMRect(rect.x, yPos, rect.width, rect.height); + }; + + scrollHandler = () => { + popup?.[0].setProps({ + getReferenceClientRect, + }); + }; + + view.dom.parentElement?.addEventListener('scroll', scrollHandler); + + popup?.[0].setProps({ + getReferenceClientRect, + appendTo: () => document.body, + content: component.element, + }); + + popup?.[0].show(); + }, + + onUpdate(props: SuggestionProps) { + component.updateProps(props); + + const { view } = props.editor; + + const editorNode = view.dom; + + const getReferenceClientRect = () => { + if (!props.clientRect) { + return props.editor.storage[extensionName].rect; + } + + const rect = props.clientRect(); + + if (!rect) { + return props.editor.storage[extensionName].rect; + } + + // Account for when the editor is bound inside a container that doesn't go all the way to the edge of the screen + return new DOMRect(rect.x, rect.y, rect.width, rect.height); + }; + + const scrollHandler = () => { + popup?.[0].setProps({ + getReferenceClientRect, + }); + }; + + view.dom.parentElement?.addEventListener('scroll', scrollHandler); + + // eslint-disable-next-line no-param-reassign + props.editor.storage[extensionName].rect = props.clientRect + ? getReferenceClientRect() + : { + width: 0, + height: 0, + left: 0, + top: 0, + right: 0, + bottom: 0, + }; + popup?.[0].setProps({ + getReferenceClientRect, + }); + }, + + onKeyDown(props: SuggestionKeyDownProps) { + if (props.event.key === 'Escape') { + popup?.[0].hide(); + + return true; + } + + if (!popup?.[0].state.isShown) { + popup?.[0].show(); + } + + return component.ref?.onKeyDown(props); + }, + + onExit(props) { + popup?.[0].hide(); + if (scrollHandler) { + const { view } = props.editor; + view.dom.parentElement?.removeEventListener( + 'scroll', + scrollHandler, + ); + } + component.destroy(); + }, + }; + }, + }), + ]; + }, + + addStorage() { + return { + rect: { + width: 0, + height: 0, + left: 0, + top: 0, + right: 0, + bottom: 0, + }, + }; + }, +}); + +export default SlashCommand; diff --git a/lib/block-editor/extensions/SlashCommand/groups.ts b/lib/block-editor/extensions/SlashCommand/groups.ts new file mode 100644 index 00000000..52fa1f05 --- /dev/null +++ b/lib/block-editor/extensions/SlashCommand/groups.ts @@ -0,0 +1,147 @@ +import { type Group } from './types'; + +export const GROUPS: Group[] = [ + { + name: 'variables', + title: 'Variables', + commands: [ + { + name: 'addVariable', + label: 'Add Variable', + iconName: 'Sparkles', + description: 'Insert a variable', + shouldBeHidden: (editor) => editor.isActive('columns'), + action: (editor) => () => {}, + }, + ], + }, + { + name: 'format', + title: 'Format', + commands: [ + { + name: 'heading1', + label: 'Heading 1', + iconName: 'Heading1', + description: 'High priority section title', + aliases: ['h1'], + action: (editor) => { + editor.chain().focus().setHeading({ level: 1 }).run(); + }, + }, + { + name: 'heading2', + label: 'Heading 2', + iconName: 'Heading2', + description: 'Medium priority section title', + aliases: ['h2'], + action: (editor) => { + editor.chain().focus().setHeading({ level: 2 }).run(); + }, + }, + { + name: 'heading3', + label: 'Heading 3', + iconName: 'Heading3', + description: 'Low priority section title', + aliases: ['h3'], + action: (editor) => { + editor.chain().focus().setHeading({ level: 3 }).run(); + }, + }, + { + name: 'bulletList', + label: 'Bullet List', + iconName: 'List', + description: 'Unordered list of items', + aliases: ['ul'], + action: (editor) => { + editor.chain().focus().toggleBulletList().run(); + }, + }, + { + name: 'numberedList', + label: 'Numbered List', + iconName: 'ListOrdered', + description: 'Ordered list of items', + aliases: ['ol'], + action: (editor) => { + editor.chain().focus().toggleOrderedList().run(); + }, + }, + { + name: 'taskList', + label: 'Task List', + iconName: 'ListTodo', + description: 'Task list with todo items', + aliases: ['todo'], + action: (editor) => { + editor.chain().focus().toggleTaskList().run(); + }, + }, + { + name: 'toggleList', + label: 'Toggle List', + iconName: 'ListCollapse', + description: 'Toggles can show and hide content', + aliases: ['toggle'], + action: (editor) => { + editor.chain().focus().setDetails().run(); + }, + }, + ], + }, + { + name: 'insert', + title: 'Insert', + commands: [ + { + name: 'group', + label: 'Group', + iconName: 'Group', + description: 'Group content together in a grid', + aliases: ['grid'], + shouldBeHidden: (editor) => editor.isActive('group'), + action: (editor) => { + editor + .chain() + .focus() + .insertContent({ + type: 'group', + attrs: { columns: 1 }, + content: [], + }) + .run(); + }, + }, + { + name: 'columns', + label: 'Columns', + iconName: 'Columns2', + description: 'Add two column content', + aliases: ['cols'], + shouldBeHidden: (editor) => editor.isActive('columns'), + action: (editor) => { + editor + .chain() + .focus() + .setColumns() + .focus(editor.state.selection.head - 1) + .run(); + }, + }, + { + name: 'horizontalRule', + label: 'Horizontal Rule', + iconName: 'Minus', + description: 'Insert a horizontal divider', + aliases: ['hr'], + action: (editor) => { + editor.chain().focus().setHorizontalRule().run(); + }, + }, + ], + }, +]; + +export default GROUPS; diff --git a/lib/block-editor/extensions/SlashCommand/index.ts b/lib/block-editor/extensions/SlashCommand/index.ts new file mode 100644 index 00000000..6e4e154b --- /dev/null +++ b/lib/block-editor/extensions/SlashCommand/index.ts @@ -0,0 +1 @@ +export * from './SlashCommand' diff --git a/lib/block-editor/extensions/SlashCommand/types.ts b/lib/block-editor/extensions/SlashCommand/types.ts new file mode 100644 index 00000000..ba8f6bb6 --- /dev/null +++ b/lib/block-editor/extensions/SlashCommand/types.ts @@ -0,0 +1,25 @@ +import { type Editor } from '@tiptap/core'; + +import { type icons } from 'lucide-react'; + +export type Group = { + name: string; + title: string; + commands: Command[]; +}; + +export type Command = { + name: string; + label: string; + description: string; + aliases?: string[]; + iconName: keyof typeof icons; + action: (editor: Editor) => void; + shouldBeHidden?: (editor: Editor) => boolean; +}; + +export type MenuListProps = { + editor: Editor; + items: Group[]; + command: (command: Command) => void; +}; diff --git a/lib/block-editor/extensions/Variable/Hint/Hint.ts b/lib/block-editor/extensions/Variable/Hint/Hint.ts new file mode 100644 index 00000000..c3c7b3d8 --- /dev/null +++ b/lib/block-editor/extensions/Variable/Hint/Hint.ts @@ -0,0 +1,26 @@ +import { Node, mergeAttributes } from '@tiptap/core'; +import { ReactNodeViewRenderer } from '@tiptap/react'; +import { HintNodeView } from './HintNodeView'; + +export const HintNode = Node.create({ + name: 'hint', + group: 'variable', + content: 'inline*', + draggable: false, + disableDropCursor: true, + + parseHTML() { + return [ + { + tag: 'hint', + }, + ]; + }, + + renderHTML({ HTMLAttributes }) { + return ['hint', mergeAttributes(HTMLAttributes)]; + }, + addNodeView() { + return ReactNodeViewRenderer(HintNodeView); + }, +}); diff --git a/lib/block-editor/extensions/Variable/Hint/HintNodeView.tsx b/lib/block-editor/extensions/Variable/Hint/HintNodeView.tsx new file mode 100644 index 00000000..006c393f --- /dev/null +++ b/lib/block-editor/extensions/Variable/Hint/HintNodeView.tsx @@ -0,0 +1,23 @@ +import { + NodeViewContent, + type NodeViewProps, + NodeViewWrapper, +} from '@tiptap/react'; +import { cn } from '~/lib/utils'; + +export const HintNodeView: React.FC = ({ node }) => { + const isEmpty = node.content.size === 0; + + return ( + + + + ); +}; diff --git a/lib/block-editor/extensions/Variable/Label/Label.ts b/lib/block-editor/extensions/Variable/Label/Label.ts new file mode 100644 index 00000000..ae718562 --- /dev/null +++ b/lib/block-editor/extensions/Variable/Label/Label.ts @@ -0,0 +1,26 @@ +// basically extends p tag to be an editable paragraph above the control +import { Node, mergeAttributes } from '@tiptap/core'; +import { ReactNodeViewRenderer } from '@tiptap/react'; +import { LabelNodeView } from './LabelNodeView'; + +export const LabelNode = Node.create({ + name: 'label', + group: 'variable', + content: 'inline*', + draggable: false, + disableDropCursor: true, + + parseHTML() { + return [ + { + tag: 'label', + }, + ]; + }, + renderHTML({ HTMLAttributes }) { + return ['label', mergeAttributes(HTMLAttributes)]; + }, + addNodeView() { + return ReactNodeViewRenderer(LabelNodeView); + }, +}); diff --git a/lib/block-editor/extensions/Variable/Label/LabelNodeView.tsx b/lib/block-editor/extensions/Variable/Label/LabelNodeView.tsx new file mode 100644 index 00000000..e0c4d3a7 --- /dev/null +++ b/lib/block-editor/extensions/Variable/Label/LabelNodeView.tsx @@ -0,0 +1,24 @@ +import { + NodeViewContent, + type NodeViewProps, + NodeViewWrapper, +} from '@tiptap/react'; +import { cn } from '~/lib/utils'; + +export const LabelNodeView: React.FC = ({ node }) => { + const isEmpty = node.content.size === 0; + + return ( + // this prevents dragging the label node out of the variable + + + + ); +}; diff --git a/lib/block-editor/extensions/Variable/Variable.ts b/lib/block-editor/extensions/Variable/Variable.ts new file mode 100644 index 00000000..a272929c --- /dev/null +++ b/lib/block-editor/extensions/Variable/Variable.ts @@ -0,0 +1,64 @@ +import { Node, mergeAttributes } from '@tiptap/core'; +import { ReactNodeViewRenderer } from '@tiptap/react'; +import { HintNode } from './Hint/Hint'; +import { LabelNode } from './Label/Label'; +import { VariableNodeView } from './VariableNodeView'; + +export type VariableNodeAttributes = { + type: string; + control?: string; + options: string[]; + name: string; + required: boolean; +}; + +export const VariableNode = Node.create({ + name: 'variable', + + group: 'block', + content: 'label hint?', + draggable: true, + selectable: true, + isolating: true, + disableDropCursor: true, + + parseHTML() { + return [ + { + tag: 'variable', + }, + ]; + }, + + addAttributes() { + return { + type: { default: 'text' }, + control: { default: null }, + options: { default: [] }, + name: { default: '' }, + required: { default: false }, + }; + }, + + addExtensions() { + // adds nested label and control nodes + return [LabelNode, HintNode]; + }, + + renderHTML({ HTMLAttributes }) { + return [ + 'variable', + mergeAttributes( + { + 'data-type': 'variable', + }, + HTMLAttributes, + ), + 0, + ]; + }, + + addNodeView() { + return ReactNodeViewRenderer(VariableNodeView); + }, +}); diff --git a/lib/block-editor/extensions/Variable/VariableMenu.tsx b/lib/block-editor/extensions/Variable/VariableMenu.tsx new file mode 100644 index 00000000..dce33919 --- /dev/null +++ b/lib/block-editor/extensions/Variable/VariableMenu.tsx @@ -0,0 +1,130 @@ +import { useEditorState, type Editor } from '@tiptap/react'; +import { ChevronDown, CircleAlert, Trash } from 'lucide-react'; +import DropdownMenu from '~/components/DropdownMenu'; +import Toolbar from '~/components/Toolbar'; + +type VariableEditorState = { + type: 'categorical' | 'text'; + control: 'checkboxGroup' | 'toggleButtonGroup' | 'text' | 'textArea'; + required: boolean; +}; + +export default function VariableMenu({ editor }: { editor: Editor | null }) { + const { type, control, required } = useEditorState({ + editor, + selector: (ctx) => { + return { + type: ctx.editor?.getAttributes('variable')?.type as + | 'categorical' + | 'text' + | 'number' + | undefined, + control: ctx.editor?.getAttributes('variable')?.control as + | 'checkboxGroup' + | 'toggleButtonGroup' + | 'text' + | 'textArea', + required: ctx.editor?.getAttributes('variable')?.required as boolean, + }; + }, + }) as VariableEditorState; + + if (!editor) { + return null; + } + + return ( + <> + + + + Select an input control + {type === 'categorical' && ( + <> + + editor?.commands.updateAttributes('variable', { + control: 'checkboxGroup', + }) + } + > + Checkbox Group + + + editor?.commands.updateAttributes('variable', { + control: 'toggleButtonGroup', + }) + } + > + Toggle Button Group + + > + )} + {type === 'text' && ( + <> + + editor?.commands.updateAttributes('variable', { + control: 'text', + }) + } + > + Text + + + editor?.commands.updateAttributes('variable', { + control: 'textArea', + }) + } + > + Text Area + + > + )} + + + + + + {control} + + + + + {/* required toggle */} + + editor?.commands.updateAttributes('variable', { + required: !required, + }) + } + > + + Required + + + { + editor?.commands.deleteNode('variable'); + }} + > + + + > + ); +} diff --git a/lib/block-editor/extensions/Variable/VariableNodeView.tsx b/lib/block-editor/extensions/Variable/VariableNodeView.tsx new file mode 100644 index 00000000..e5d77064 --- /dev/null +++ b/lib/block-editor/extensions/Variable/VariableNodeView.tsx @@ -0,0 +1,87 @@ +import { Label } from '@radix-ui/react-label'; +import { + NodeViewContent, + type NodeViewProps, + NodeViewWrapper, + useEditorState, +} from '@tiptap/react'; +import { AlertCircle } from 'lucide-react'; +import { Input } from '~/components/form/Input'; +import { cn } from '~/lib/utils'; +import { type VariableNodeAttributes } from './Variable'; + +export const VariableNodeView: React.FC = ({ node, editor }) => { + const { type, control, options, name, required } = + node.attrs as VariableNodeAttributes; + + // get selected here instead of from props to get updated value + const { selected } = useEditorState({ + editor, + selector: (ctx) => { + return { + selected: ctx.editor?.isActive('variable', { + name, + }), + }; + }, + }); + + const renderControl = () => { + switch (type) { + case 'text': + return control === 'textArea' ? ( + + ) : ( + + ); + + case 'number': + return ; + + case 'categorical': + return control === 'checkboxGroup' ? ( + + {options.map((option: string) => ( + + + {option} + + ))} + + ) : ( + + {options.map((option: string) => ( + + {option} + + ))} + + ); + + default: + return null; + } + }; + + return ( + + + + {required && ( + + Required + + )} + + {renderControl()} + + ); +}; diff --git a/lib/block-editor/extensions/Variable/index.ts b/lib/block-editor/extensions/Variable/index.ts new file mode 100644 index 00000000..21cddc23 --- /dev/null +++ b/lib/block-editor/extensions/Variable/index.ts @@ -0,0 +1 @@ +export * from './Variable'; diff --git a/lib/block-editor/extensions/Variable/utils.ts b/lib/block-editor/extensions/Variable/utils.ts new file mode 100644 index 00000000..c7e37166 --- /dev/null +++ b/lib/block-editor/extensions/Variable/utils.ts @@ -0,0 +1,74 @@ +import { type EditorView } from '@tiptap/pm/view'; +import type { TVariableDefinition } from '~/schemas/protocol/variables'; + +export const createVariableNode = ({ + newVariable, + view, + key, +}: { + newVariable: TVariableDefinition; + view: EditorView; + key: string; +}) => { + const { label, hint, variable } = view.state.schema.nodes; + // create the label node + const labelNode = label?.create( + {}, + view.state.schema.text(newVariable.label.en ?? key), // todo: integrate with internationalization + ); + + // create the hint node + const hintNode = hint?.create( + {}, // empty so that it will show the placeholder + ); + + const optionsLabels = + newVariable.type === 'categorical' + ? (newVariable.options?.map( + (option: { label: string }) => option.label, + ) ?? []) + : []; + + // Create the parent variable node with label and control + if (!variable || !labelNode || !hintNode) return false; + + const variableNode = variable.create( + { + type: newVariable.type, + options: optionsLabels, + name: key, + control: newVariable.control, + }, + [labelNode, hintNode], + ); + + return variableNode; +}; + +export const handleVariableDrop = (view: EditorView, event: DragEvent) => { + // Get the variable data + const jsonData = event.dataTransfer?.getData('application/json'); + if (!jsonData) return false; + const newVariable = JSON.parse(jsonData) as TVariableDefinition; + const key = event.dataTransfer?.getData('application/x-variable-key'); + + if (!key) return false; + + const variableNode = createVariableNode({ + newVariable, + view, + key, + }); + + const coordinates = view.posAtCoords({ + left: event.clientX, + top: event.clientY, + }); + + if (!coordinates || !variableNode) return false; + + const transaction = view.state.tr.insert(coordinates.pos, variableNode); + view.dispatch(transaction); + + return true; +}; diff --git a/lib/block-editor/extensions/extension-kit.ts b/lib/block-editor/extensions/extension-kit.ts new file mode 100644 index 00000000..2e0eccab --- /dev/null +++ b/lib/block-editor/extensions/extension-kit.ts @@ -0,0 +1,101 @@ +import { mergeAttributes } from '@tiptap/core'; +import BulletList from '@tiptap/extension-bullet-list'; +import Document from '@tiptap/extension-document'; +import Heading from '@tiptap/extension-heading'; +import Paragraph from '@tiptap/extension-paragraph'; +import Text from '@tiptap/extension-text'; +import Typography from '@tiptap/extension-typography'; +import StarterKit from '@tiptap/starter-kit'; +import { Underline } from 'lucide-react'; +import AutoJoiner from 'tiptap-extension-auto-joiner'; +import GlobalDragHandle from 'tiptap-extension-global-drag-handle'; +import { headingVariants } from '~/components/typography/Heading'; +import { paragraphVariants } from '~/components/typography/Paragraph'; +import { unorderedListClasses } from '~/components/typography/UnorderedList'; +import { Dropcursor } from './Dropcursor'; +import { Group } from './Group'; +import { HorizontalRule } from './HorizontalRule'; +import { ImageBlock } from './ImageBlock'; +import { Link } from './Link'; +import { Column, Columns } from './MultiColumn'; +import { Selection } from './Selection'; +import { SlashCommand } from './SlashCommand'; +import { VariableNode } from './Variable'; + +export const ExtensionKit = () => [ + StarterKit.configure({ + document: false, + dropcursor: false, + heading: false, + paragraph: false, + text: false, + bulletList: false, + horizontalRule: false, + blockquote: false, + history: false, + codeBlock: false, + }), + Document, + Heading.extend({ + levels: [1, 2, 3, 4], + renderHTML({ node, HTMLAttributes }) { + type HeadingAttrs = { + level: string; + }; + + const { level } = node.attrs as HeadingAttrs; + const nodeLevel = parseInt(level, 10); + + const variant = this.options.levels.includes(nodeLevel) + ? (`h${nodeLevel}` as 'h1' | 'h2' | 'h3' | 'h4') + : 'h1'; + + const classes = headingVariants({ variant }); + + return [ + `h${level}`, + mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { + class: classes, + }), + 0, + ]; + }, + }).configure({ levels: [1, 2, 3, 4] }), + Paragraph.configure({ + HTMLAttributes: { + class: paragraphVariants(), + }, + }), + Text, + BulletList.configure({ + HTMLAttributes: { + class: unorderedListClasses, + }, + }), + GlobalDragHandle.configure({ + customNodes: ['variable', 'group'], // customNodes is required for dragging custom nodes + }), + AutoJoiner, // Recommended by GlobalDragHandle author. Allows merging nodes when dragging. + Columns, + Column, + Selection, + HorizontalRule, + Link.configure({ + openOnClick: false, + autolink: true, + defaultProtocol: 'https', + }), + Underline, + ImageBlock, + Typography, + SlashCommand, + Dropcursor.configure({ + // Shows a placeholder for where dragged content will be inserted. + width: 2, + class: '', + }), + VariableNode, + Group, +]; + +export default ExtensionKit; diff --git a/lib/block-editor/useBlockEditor.tsx b/lib/block-editor/useBlockEditor.tsx new file mode 100644 index 00000000..aea3fd5e --- /dev/null +++ b/lib/block-editor/useBlockEditor.tsx @@ -0,0 +1,57 @@ +import { useEditor } from '@tiptap/react'; +import ExtensionKit from './extensions/extension-kit'; +import { handleDrop } from './utils'; + +export const useBlockEditor = () => { + const editor = useEditor( + { + immediatelyRender: true, + shouldRerenderOnTransaction: false, + autofocus: true, + // onCreate: (ctx) => {}, + extensions: [...ExtensionKit()], + editorProps: { + attributes: { + autocomplete: 'off', + autocorrect: 'off', + autocapitalize: 'off', + class: 'min-h-full focus:outline-none', + }, + handleDrop: function (view, event) { + if ( + event?.dataTransfer?.types.includes('application/x-content-type') + ) { + handleDrop(view, event, editor); + } + + return false; + }, + }, + + content: ` + + Welcome to the Block Editor! + + + This is a paragraph block. + + + Another heading + + + Text following h2. + + Unordered list itemanother item + + + Variable Label + This is a hint + + + `, + }, + [], // Dependency array + ); + + return { editor }; +}; diff --git a/lib/block-editor/utils.ts b/lib/block-editor/utils.ts new file mode 100644 index 00000000..0a6f8e33 --- /dev/null +++ b/lib/block-editor/utils.ts @@ -0,0 +1,115 @@ +import { type EditorView } from '@tiptap/pm/view'; +import { type Editor } from '@tiptap/react'; +import type { TVariableDefinition } from '~/schemas/protocol/variables'; +import { contentMap, type TiptapContent } from './contentTypes'; +import { handleVariableDrop } from './extensions/Variable/utils'; + +export const getRenderContainer = (editor: Editor, nodeType: string) => { + const { + view, + state: { + selection: { from }, + }, + } = editor; + + const elements = document.querySelectorAll('.has-focus'); + const elementCount = elements.length; + const innermostNode = elements[elementCount - 1]; + const element = innermostNode; + + if ( + (element?.getAttribute('data-type') && + element.getAttribute('data-type') === nodeType) ?? + element?.classList?.contains(nodeType) + ) { + return element; + } + + const node = view.domAtPos(from).node as HTMLElement; + let container: HTMLElement | null = node; + + if (!container.tagName) { + container = node.parentElement; + } + + while ( + container && + !( + container.getAttribute('data-type') && + container.getAttribute('data-type') === nodeType + ) && + !container.classList.contains(nodeType) + ) { + container = container.parentElement; + } + + return container; +}; + +export const handleDrag = ( + event: React.DragEvent, + type: TiptapContent, + data?: TVariableDefinition | null, + key?: string, +) => { + event.dataTransfer.setData('application/x-content-type', type); + + // If it's a variable, add key and data + if (type === 'variable' && data && key) { + event.dataTransfer.setData('application/json', JSON.stringify(data)); + event.dataTransfer.setData('application/x-variable-key', key); + } +}; + +export const handleDrop = ( + view: EditorView, + event: DragEvent, + editor: Editor, +) => { + const type = event?.dataTransfer?.getData('application/x-content-type'); + const pos = view.posAtCoords({ + left: event.clientX, + top: event.clientY, + }); + + if (!isValidDropPosition(view, event)) return false; + + if (pos && editor) { + if (!type) return; + if (type === 'variable') { + handleVariableDrop(view, event); + } else { + if (!contentMap[type]) return; + editor.chain().focus().insertContentAt(pos.pos, contentMap[type]).run(); + } + return true; + } +}; + +export const isValidDropPosition = (view: EditorView, event: DragEvent) => { + // Get the drop position + const coordinates = view.posAtCoords({ + left: event.clientX, + top: event.clientY, + }); + + if (!coordinates) return false; + + const dropPos = coordinates.pos; + const resolvedDropPos = view.state.doc.resolve(dropPos); + + // Drop is valid if it's between nodes or the parent is the document + const isBetweenNodes = + resolvedDropPos.nodeBefore === null || // node start + resolvedDropPos.nodeAfter === null; // node end + + if ( + resolvedDropPos.parent.isTextblock || // Prevent dropping inside p, h1, h2, etc. + resolvedDropPos.parent.type.name === 'bulletList' || // Prevent dropping inside bullet lists + resolvedDropPos.parent.type.name === 'variable' // Prevent dropping inside other variables + ) { + return false; + } + + return isBetweenNodes || resolvedDropPos.parent.type.name === 'doc'; +}; diff --git a/lib/db/sample-data/dev-protocol.ts b/lib/db/sample-data/dev-protocol.ts index fcd76ebb..0a991e4b 100644 --- a/lib/db/sample-data/dev-protocol.ts +++ b/lib/db/sample-data/dev-protocol.ts @@ -15,6 +15,7 @@ export const devProtocol: Protocol = { // TODO: should validation go here, or with the form? required: true, }, + control: 'text', }, age: { type: 'number', @@ -27,6 +28,19 @@ export const devProtocol: Protocol = { required: true, }, }, + school: { + type: 'categorical', + label: { + en: 'School', + es: 'Escuela', + ar: 'المدرسة', + }, + options: [ + { value: 'Northwestern', label: 'Northwestern' }, + { value: 'U Chicago', label: 'U Chicago' }, + ], + control: 'checkboxGroup', + }, }, forms: { familyMember: [ diff --git a/lib/dialogs/Dialog.stories.tsx b/lib/dialogs/Dialog.stories.tsx index 816b77d2..354079db 100644 --- a/lib/dialogs/Dialog.stories.tsx +++ b/lib/dialogs/Dialog.stories.tsx @@ -1,8 +1,8 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { Dialog, type DialogProps } from './Dialog'; import { fn } from '@storybook/test'; -import Form from '~/components/ui/form/Form'; -import { Button } from '~/components/ui/Button'; +import { Button } from '~/components/Button'; +import Form from '~/components/form/Form'; +import { Dialog, type DialogProps } from './Dialog'; const meta: Meta = { title: 'Systems/Dialogs/Dialog', diff --git a/lib/dialogs/Dialog.tsx b/lib/dialogs/Dialog.tsx index 57f0984d..238f0de6 100644 --- a/lib/dialogs/Dialog.tsx +++ b/lib/dialogs/Dialog.tsx @@ -1,9 +1,9 @@ +import React, { useId } from 'react'; +import CloseButton from '~/components/CloseButton'; import Surface from '~/components/layout/Surface'; -import { cn } from '../utils'; -import Paragraph from '~/components/typography/Paragraph'; -import CloseButton from '~/components/ui/CloseButton'; import Heading from '~/components/typography/Heading'; -import React, { useId } from 'react'; +import Paragraph from '~/components/typography/Paragraph'; +import { cn } from '../utils'; export type DialogProps = { title: string; diff --git a/lib/dialogs/DialogProvider.tsx b/lib/dialogs/DialogProvider.tsx index 57910bc5..a2e52f07 100644 --- a/lib/dialogs/DialogProvider.tsx +++ b/lib/dialogs/DialogProvider.tsx @@ -1,5 +1,6 @@ 'use client'; +import { useTranslations } from 'next-intl'; import React, { createContext, useCallback, @@ -7,12 +8,11 @@ import React, { useState, type RefObject, } from 'react'; -import { generatePublicId } from '../generatePublicId'; import { flushSync } from 'react-dom'; +import { Button } from '~/components/Button'; +import Form from '~/components/form/Form'; +import { generatePublicId } from '../generatePublicId'; import { Dialog } from './Dialog'; -import { useTranslations } from 'next-intl'; -import Form from '~/components/ui/form/Form'; -import { Button } from '~/components/ui/Button'; type ConfirmDialog = { type: 'confirm'; diff --git a/lib/dialogs/useDialog.stories.tsx b/lib/dialogs/useDialog.stories.tsx index d8710120..b4416327 100644 --- a/lib/dialogs/useDialog.stories.tsx +++ b/lib/dialogs/useDialog.stories.tsx @@ -1,9 +1,9 @@ /* eslint-disable no-console */ import type { StoryObj } from '@storybook/react'; -import { Button } from '~/components/ui/Button'; -import { useDialog } from './DialogProvider'; -import Form from '~/components/ui/form/Form'; import { fn } from '@storybook/test'; +import { Button } from '~/components/Button'; +import Form from '~/components/form/Form'; +import { useDialog } from './DialogProvider'; const meta = { title: 'Systems/Dialogs/useDialog', diff --git a/lib/localisation/config.ts b/lib/localisation/config.ts index d7aacee6..dad3c404 100644 --- a/lib/localisation/config.ts +++ b/lib/localisation/config.ts @@ -1,6 +1,6 @@ import { type Locale } from '~/schemas/protocol/i18n'; -export const FALLBACK_LOCALE = 'en' as const; +export const FALLBACK_LOCALE = 'en'; // Locales we provide for our backend. For now, english, spanish, and arabic // for testing RTL support. diff --git a/lib/localisation/utils.ts b/lib/localisation/utils.ts index 4e83fd96..94ef966c 100644 --- a/lib/localisation/utils.ts +++ b/lib/localisation/utils.ts @@ -3,13 +3,13 @@ import { type IntlError, IntlErrorCode, } from 'next-intl'; -import type { LocalisedRecord, LocalisedString } from '../../schemas/shared'; import { type Locale, - type LocaleCookieName, type LocaleObject, SUPPORTED_LOCALE_OBJECTS, -} from './config'; +} from '~/schemas/protocol/i18n'; +import type { LocalisedRecord, LocalisedString } from '../../schemas/shared'; +import { type LocaleCookieName } from './config'; export const customErrorLogger = (error: IntlError) => { if (error.code === IntlErrorCode.MISSING_MESSAGE) { diff --git a/package.json b/package.json index c2d6e44b..d255b88a 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@radix-ui/react-accordion": "^1.2.1", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-direction": "^1.1.0", + "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-popper": "^1.2.0", @@ -31,10 +32,28 @@ "@radix-ui/react-select": "^2.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-switch": "^1.1.0", + "@radix-ui/react-toolbar": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.2", "@t3-oss/env-nextjs": "^0.11.1", "@tailwindcss/aspect-ratio": "^0.4.2", "@tailwindcss/container-queries": "^0.1.1", + "@tiptap/core": "^2.9.1", + "@tiptap/extension-bullet-list": "^2.9.1", + "@tiptap/extension-document": "^2.9.1", + "@tiptap/extension-dropcursor": "^2.9.1", + "@tiptap/extension-heading": "^2.9.1", + "@tiptap/extension-horizontal-rule": "^2.9.1", + "@tiptap/extension-image": "^2.9.1", + "@tiptap/extension-link": "^2.9.1", + "@tiptap/extension-list-item": "^2.9.1", + "@tiptap/extension-paragraph": "^2.9.1", + "@tiptap/extension-text": "^2.9.1", + "@tiptap/extension-typography": "^2.9.1", + "@tiptap/extension-underline": "^2.9.1", + "@tiptap/pm": "^2.9.1", + "@tiptap/react": "^2.9.1", + "@tiptap/starter-kit": "^2.9.1", + "@tiptap/suggestion": "^2.9.1", "@types/rtl-detect": "^1.0.3", "@types/validator": "^13.12.1", "@typescript-eslint/eslint-plugin": "^7.18.0", @@ -64,6 +83,9 @@ "sharp": "^0.33.5", "tailwind-merge": "^2.5.2", "tailwind-variants": "^0.2.1", + "tippy.js": "^6.3.7", + "tiptap-extension-auto-joiner": "^0.1.3", + "tiptap-extension-global-drag-handle": "^0.1.15", "type-fest": "^4.26.1", "validator": "^13.12.0", "zod": "^3.23.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f49a7b4..6404cb63 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -31,6 +31,9 @@ importers: '@radix-ui/react-direction': specifier: ^1.1.0 version: 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.2 + version: 2.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) '@radix-ui/react-label': specifier: ^2.1.0 version: 2.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) @@ -52,6 +55,9 @@ importers: '@radix-ui/react-switch': specifier: ^1.1.0 version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-toolbar': + specifier: ^1.1.0 + version: 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) '@radix-ui/react-tooltip': specifier: ^1.1.2 version: 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) @@ -64,6 +70,57 @@ importers: '@tailwindcss/container-queries': specifier: ^0.1.1 version: 0.1.1(tailwindcss@3.4.12) + '@tiptap/core': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/extension-bullet-list': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-document': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-dropcursor': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) + '@tiptap/extension-heading': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-horizontal-rule': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) + '@tiptap/extension-image': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-link': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) + '@tiptap/extension-list-item': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-paragraph': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-text': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-typography': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-underline': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/pm': + specifier: ^2.9.1 + version: 2.9.1 + '@tiptap/react': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@tiptap/starter-kit': + specifier: ^2.9.1 + version: 2.9.1 + '@tiptap/suggestion': + specifier: ^2.9.1 + version: 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) '@types/rtl-detect': specifier: ^1.0.3 version: 1.0.3 @@ -151,6 +208,15 @@ importers: tailwind-variants: specifier: ^0.2.1 version: 0.2.1(tailwindcss@3.4.12) + tippy.js: + specifier: ^6.3.7 + version: 6.3.7 + tiptap-extension-auto-joiner: + specifier: ^0.1.3 + version: 0.1.3 + tiptap-extension-global-drag-handle: + specifier: ^0.1.15 + version: 0.1.15 type-fest: specifier: ^4.26.1 version: 4.26.1 @@ -1778,6 +1844,9 @@ packages: webpack-plugin-serve: optional: true + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + '@prisma/client@5.19.1': resolution: {integrity: sha512-x30GFguInsgt+4z5I4WbkZP2CGpotJMUXy+Gl/aaUjHn2o1DnLYNTA+q9XdYmAQZM8fIIkvUiA2NpgosM3fneg==} engines: {node: '>=16.13'} @@ -1922,6 +1991,32 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-dismissable-layer@1.1.1': + resolution: {integrity: sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.2': + resolution: {integrity: sha512-GVZMR+eqK8/Kes0a36Qrv+i20bAPXSn8rCBTHx30w+3ECnR5o3xixAlqcVaYvLeyKUsm0aqyhWfmUcqufM8nYA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-focus-guards@1.1.0': resolution: {integrity: sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==} peerDependencies: @@ -1931,6 +2026,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-focus-guards@1.1.1': + resolution: {integrity: sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-focus-scope@1.1.0': resolution: {integrity: sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==} peerDependencies: @@ -1966,6 +2070,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-menu@2.1.2': + resolution: {integrity: sha512-lZ0R4qR2Al6fZ4yCCZzu/ReTFrylHFxIqy7OezIpWF4bL0o9biKo0pFIvkaew3TyZ9Fy5gYVrR5zCGZBVbO1zg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-popover@1.1.1': resolution: {integrity: sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==} peerDependencies: @@ -2005,6 +2122,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-portal@1.1.2': + resolution: {integrity: sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-presence@1.1.0': resolution: {integrity: sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==} peerDependencies: @@ -2083,6 +2213,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-separator@1.1.0': + resolution: {integrity: sha512-3uBAs+egzvJBDZAzvb/n4NxxOYpnspmWxO2u5NbZ8Y6FM/NdrGSF9bop3Cf6F6C71z1rTSn8KV0Fo2ZVd79lGA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-slot@1.1.0': resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==} peerDependencies: @@ -2105,6 +2248,45 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-toggle-group@1.1.0': + resolution: {integrity: sha512-PpTJV68dZU2oqqgq75Uzto5o/XfOVgkrJ9rulVmfTKxWp3HfUjHE6CP/WLRR4AzPX9HWxw7vFow2me85Yu+Naw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.0': + resolution: {integrity: sha512-gwoxaKZ0oJ4vIgzsfESBuSgJNdc0rv12VhHgcqN0TEJmmZixXG/2XpsLK8kzNWYcnaoRIEEQc0bEi3dIvdUpjw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toolbar@1.1.0': + resolution: {integrity: sha512-ZUKknxhMTL/4hPh+4DuaTot9aO7UD6Kupj4gqXCsBTayX1pD1L+0C2/2VZKXb4tIifQklZ3pf2hG9T+ns+FclQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-tooltip@1.1.2': resolution: {integrity: sha512-9XRsLwe6Yb9B/tlnYCPVUd/TFS4J7HuOZW345DCeC6vKIxQGMZdx21RK4VoZauPD5frgkXTYVS5y90L+3YBn4w==} peerDependencies: @@ -2197,6 +2379,9 @@ packages: '@radix-ui/rect@1.1.0': resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==} + '@remirror/core-constants@3.0.0': + resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==} + '@rollup/rollup-android-arm-eabi@4.24.0': resolution: {integrity: sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==} cpu: [arm] @@ -2561,6 +2746,164 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@tiptap/core@2.9.1': + resolution: {integrity: sha512-tifnLL/ARzQ6/FGEJjVwj9UT3v+pENdWHdk9x6F3X0mB1y0SeCjV21wpFLYESzwNdBPAj8NMp8Behv7dBnhIfw==} + peerDependencies: + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-blockquote@2.9.1': + resolution: {integrity: sha512-Y0jZxc/pdkvcsftmEZFyG+73um8xrx6/DMfgUcNg3JAM63CISedNcr+OEI11L0oFk1KFT7/aQ9996GM6Kubdqg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-bold@2.9.1': + resolution: {integrity: sha512-e2P1zGpnnt4+TyxTC5pX/lPxPasZcuHCYXY0iwQ3bf8qRQQEjDfj3X7EI+cXqILtnhOiviEOcYmeu5op2WhQDg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-bubble-menu@2.9.1': + resolution: {integrity: sha512-DWUF6NG08/bZDWw0jCeotSTvpkyqZTi4meJPomG9Wzs/Ol7mEwlNCsCViD999g0+IjyXFatBk4DfUq1YDDu++Q==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-bullet-list@2.9.1': + resolution: {integrity: sha512-0hizL/0j9PragJObjAWUVSuGhN1jKjCFnhLQVRxtx4HutcvS/lhoWMvFg6ZF8xqWgIa06n6A7MaknQkqhTdhKA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-code-block@2.9.1': + resolution: {integrity: sha512-A/50wPWDqEUUUPhrwRKILP5gXMO5UlQ0F6uBRGYB9CEVOREam9yIgvONOnZVJtszHqOayjIVMXbH/JMBeq11/g==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-code@2.9.1': + resolution: {integrity: sha512-WQqcVGe7i/E+yO3wz5XQteU1ETNZ00euUEl4ylVVmH2NM4Dh0KDjEhbhHlCM0iCfLUo7jhjC7dmS+hMdPUb+Tg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-document@2.9.1': + resolution: {integrity: sha512-1a+HCoDPnBttjqExfYLwfABq8MYdiowhy/wp8eCxVb6KGFEENO53KapstISvPzqH7eOi+qRjBB1KtVYb/ZXicg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-dropcursor@2.9.1': + resolution: {integrity: sha512-wJZspSmJRkDBtPkzFz1g7gvZOEOayk8s93UHsgbJxcV4VWHYleZ5XhT74sZunSjefNDm3qC6v2BSgLp3vNHVKQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-floating-menu@2.10.3': + resolution: {integrity: sha512-Prg8rYLxeyzHxfzVu1mDkkUWMnD9ZN3y370O/1qy55e+XKVw9jFkTSuz0y0+OhMJG6bulYpDUMtb+N3+2xOWlQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-gapcursor@2.9.1': + resolution: {integrity: sha512-jsRBmX01vr+5H02GljiHMo0n5H1vzoMLmFarxe0Yq2d2l9G/WV2VWX2XnGliqZAYWd1bI0phs7uLQIN3mxGQTw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-hard-break@2.9.1': + resolution: {integrity: sha512-fCuaOD/b7nDjm47PZ58oanq7y4ccS2wjPh42Qm0B0yipu/1fmC8eS1SmaXmk28F89BLtuL6uOCtR1spe+lZtlQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-heading@2.9.1': + resolution: {integrity: sha512-SjZowzLixOFaCrV2cMaWi1mp8REK0zK1b3OcVx7bCZfVSmsOETJyrAIUpCKA8o60NwF7pwhBg0MN8oXlNKMeFw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-history@2.9.1': + resolution: {integrity: sha512-wp9qR1NM+LpvyLZFmdNaAkDq0d4jDJ7z7Fz7icFQPu31NVxfQYO3IXNmvJDCNu8hFAbImpA5aG8MBuwzRo0H9w==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-horizontal-rule@2.9.1': + resolution: {integrity: sha512-ydUhABeaBI1CoJp+/BBqPhXINfesp1qMNL/jiDcMsB66fsD4nOyphpAJT7FaRFZFtQVF06+nttBtFZVkITQVqg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-image@2.9.1': + resolution: {integrity: sha512-aGqJnsuS8oagIhsx7wetm8jw4NEDsOV0OSx4FQ4VPlUqWlnzK0N+erFKKJmXTdAxL8PGzoPSlITFH63MV3eV3Q==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-italic@2.9.1': + resolution: {integrity: sha512-VkNA6Vz96+/+7uBlsgM7bDXXx4b62T1fDam/3UKifA72aD/fZckeWrbT7KrtdUbzuIniJSbA0lpTs5FY29+86Q==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-link@2.9.1': + resolution: {integrity: sha512-yG+e3e8cCCN9dZjX4ttEe3e2xhh58ryi3REJV4MdiEkOT9QF75Bl5pUbMIS4tQ8HkOr04QBFMHKM12kbSxg1BA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + + '@tiptap/extension-list-item@2.9.1': + resolution: {integrity: sha512-6O4NtYNR5N2Txi4AC0/4xMRJq9xd4+7ShxCZCDVL0WDVX37IhaqMO7LGQtA6MVlYyNaX4W1swfdJaqrJJ5HIUw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-ordered-list@2.9.1': + resolution: {integrity: sha512-6J9jtv1XP8dW7/JNSH/K4yiOABc92tBJtgCsgP8Ep4+fjfjdj4HbjS1oSPWpgItucF2Fp/VF8qg55HXhjxHjTw==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-paragraph@2.9.1': + resolution: {integrity: sha512-JOmT0xd4gd3lIhLwrsjw8lV+ZFROKZdIxLi0Ia05XSu4RLrrvWj0zdKMSB+V87xOWfSB3Epo95zAvnPox5Q16A==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-strike@2.9.1': + resolution: {integrity: sha512-V5aEXdML+YojlPhastcu7w4biDPwmzy/fWq0T2qjfu5Te/THcqDmGYVBKESBm5x6nBy5OLkanw2O+KHu2quDdg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-text-style@2.9.1': + resolution: {integrity: sha512-LAxc0SeeiPiAVBwksczeA7BJSZb6WtVpYhy5Esvy9K0mK5kttB4KxtnXWeQzMIJZQbza65yftGKfQlexf/Y7yg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-text@2.9.1': + resolution: {integrity: sha512-3wo9uCrkLVLQFgbw2eFU37QAa1jq1/7oExa+FF/DVxdtHRS9E2rnUZ8s2hat/IWzvPUHXMwo3Zg2XfhoamQpCA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-typography@2.9.1': + resolution: {integrity: sha512-HX0kghh+Gmlp5FsVVGmQNRxxA+aErLBgmKVspycJ3UHzAkyzsdx4qM19KCZ3pMOI+kxcXF9cMh3QxJYJ+OQ7wg==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/extension-underline@2.9.1': + resolution: {integrity: sha512-IrUsIqKPgD7GcAjr4D+RC0WvLHUDBTMkD8uPNEoeD1uH9t9zFyDfMRPnx/z3/6Gf6fTh3HzLcHGibiW2HiMi2A==} + peerDependencies: + '@tiptap/core': ^2.7.0 + + '@tiptap/pm@2.9.1': + resolution: {integrity: sha512-mvV86fr7kEuDYEApQ2uMPCKL2uagUE0BsXiyyz3KOkY1zifyVm1fzdkscb24Qy1GmLzWAIIihA+3UHNRgYdOlQ==} + + '@tiptap/react@2.9.1': + resolution: {integrity: sha512-LQJ34ZPfXtJF36SZdcn4Fiwsl2WxZ9YRJI87OLnsjJ45O+gV/PfBzz/4ap+LF8LOS0AbbGhTTjBOelPoNm+aYA==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + + '@tiptap/starter-kit@2.9.1': + resolution: {integrity: sha512-nsw6UF/7wDpPfHRhtGOwkj1ipIEiWZS1VGw+c14K61vM1CNj0uQ4jogbHwHZqN1dlL5Hh+FCqUHDPxG6ECbijg==} + + '@tiptap/suggestion@2.9.1': + resolution: {integrity: sha512-MMxwpbtocxUsbmc8qtFY1AQYNTW5i/M4aNSv9zsKKRISaS5hMD7XVrw2eod0x0yEqZU3izLiPDZPmgr8glF+jQ==} + peerDependencies: + '@tiptap/core': ^2.7.0 + '@tiptap/pm': ^2.7.0 + '@total-typescript/ts-reset@0.5.1': resolution: {integrity: sha512-AqlrT8YA1o7Ff5wPfMOL0pvL+1X+sw60NN6CcOCqs658emD6RfiXhF7Gu9QcfKBH7ELY2nInLhKSCWVoNL70MQ==} @@ -2627,9 +2970,18 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} + '@types/lodash@4.17.7': resolution: {integrity: sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==} + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/mdx@2.0.13': resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} @@ -2681,6 +3033,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + '@types/uuid@9.0.8': resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} @@ -3394,6 +3749,9 @@ packages: create-hmac@1.1.7: resolution: {integrity: sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==} + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -3622,6 +3980,10 @@ packages: entities@2.2.0: resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -4466,6 +4828,12 @@ packages: lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} + + linkifyjs@4.1.3: + resolution: {integrity: sha512-auMesunaJ8yfkHvK4gfg1K0SaKX/6Wn9g2Aac/NwX+l5VdmFZzo/hdPGxEOETj+ryRa4/fiOPjeeKURSAJx1sg==} + loader-runner@4.3.0: resolution: {integrity: sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==} engines: {node: '>=6.11.5'} @@ -4537,6 +4905,10 @@ packages: map-or-similar@1.5.0: resolution: {integrity: sha512-0aF7ZmVon1igznGI4VS30yugpduQW3y3GkcgGJOp7d8x8QrizhigUxjI/m2UojsXXto+jLAH3KSz+xOJTiORjg==} + markdown-it@14.1.0: + resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} + hasBin: true + markdown-to-jsx@7.5.0: resolution: {integrity: sha512-RrBNcMHiFPcz/iqIj0n3wclzHXjwS7mzjBNWecKKVhNTIxQepIix6Il/wZCn2Cg5Y1ow2Qi84+eJrryFRWBEWw==} engines: {node: '>= 10'} @@ -4546,6 +4918,9 @@ packages: md5.js@1.3.5: resolution: {integrity: sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + media-typer@0.3.0: resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} engines: {node: '>= 0.6'} @@ -4774,6 +5149,9 @@ packages: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} + orderedmap@2.1.1: + resolution: {integrity: sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==} + os-browserify@0.3.0: resolution: {integrity: sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==} @@ -5103,6 +5481,64 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + prosemirror-changeset@2.2.1: + resolution: {integrity: sha512-J7msc6wbxB4ekDFj+n9gTW/jav/p53kdlivvuppHsrZXCaQdVgRghoZbSS3kwrRyAstRVQ4/+u5k7YfLgkkQvQ==} + + prosemirror-collab@1.3.1: + resolution: {integrity: sha512-4SnynYR9TTYaQVXd/ieUvsVV4PDMBzrq2xPUWutHivDuOshZXqQ5rGbZM84HEaXKbLdItse7weMGOUdDVcLKEQ==} + + prosemirror-commands@1.6.2: + resolution: {integrity: sha512-0nDHH++qcf/BuPLYvmqZTUUsPJUCPBUXt0J1ErTcDIS369CTp773itzLGIgIXG4LJXOlwYCr44+Mh4ii6MP1QA==} + + prosemirror-dropcursor@1.8.1: + resolution: {integrity: sha512-M30WJdJZLyXHi3N8vxN6Zh5O8ZBbQCz0gURTfPmTIBNQ5pxrdU7A58QkNqfa98YEjSAL1HUyyU34f6Pm5xBSGw==} + + prosemirror-gapcursor@1.3.2: + resolution: {integrity: sha512-wtjswVBd2vaQRrnYZaBCbyDqr232Ed4p2QPtRIUK5FuqHYKGWkEwl08oQM4Tw7DOR0FsasARV5uJFvMZWxdNxQ==} + + prosemirror-history@1.4.1: + resolution: {integrity: sha512-2JZD8z2JviJrboD9cPuX/Sv/1ChFng+xh2tChQ2X4bB2HeK+rra/bmJ3xGntCcjhOqIzSDG6Id7e8RJ9QPXLEQ==} + + prosemirror-inputrules@1.4.0: + resolution: {integrity: sha512-6ygpPRuTJ2lcOXs9JkefieMst63wVJBgHZGl5QOytN7oSZs3Co/BYbc3Yx9zm9H37Bxw8kVzCnDsihsVsL4yEg==} + + prosemirror-keymap@1.2.2: + resolution: {integrity: sha512-EAlXoksqC6Vbocqc0GtzCruZEzYgrn+iiGnNjsJsH4mrnIGex4qbLdWWNza3AW5W36ZRrlBID0eM6bdKH4OStQ==} + + prosemirror-markdown@1.13.1: + resolution: {integrity: sha512-Sl+oMfMtAjWtlcZoj/5L/Q39MpEnVZ840Xo330WJWUvgyhNmLBLN7MsHn07s53nG/KImevWHSE6fEj4q/GihHw==} + + prosemirror-menu@1.2.4: + resolution: {integrity: sha512-S/bXlc0ODQup6aiBbWVsX/eM+xJgCTAfMq/nLqaO5ID/am4wS0tTCIkzwytmao7ypEtjj39i7YbJjAgO20mIqA==} + + prosemirror-model@1.23.0: + resolution: {integrity: sha512-Q/fgsgl/dlOAW9ILu4OOhYWQbc7TQd4BwKH/RwmUjyVf8682Be4zj3rOYdLnYEcGzyg8LL9Q5IWYKD8tdToreQ==} + + prosemirror-schema-basic@1.2.3: + resolution: {integrity: sha512-h+H0OQwZVqMon1PNn0AG9cTfx513zgIG2DY00eJ00Yvgb3UD+GQ/VlWW5rcaxacpCGT1Yx8nuhwXk4+QbXUfJA==} + + prosemirror-schema-list@1.4.1: + resolution: {integrity: sha512-jbDyaP/6AFfDfu70VzySsD75Om2t3sXTOdl5+31Wlxlg62td1haUpty/ybajSfJ1pkGadlOfwQq9kgW5IMo1Rg==} + + prosemirror-state@1.4.3: + resolution: {integrity: sha512-goFKORVbvPuAQaXhpbemJFRKJ2aixr+AZMGiquiqKxaucC6hlpHNZHWgz5R7dS4roHiwq9vDctE//CZ++o0W1Q==} + + prosemirror-tables@1.6.1: + resolution: {integrity: sha512-p8WRJNA96jaNQjhJolmbxTzd6M4huRE5xQ8OxjvMhQUP0Nzpo4zz6TztEiwk6aoqGBhz9lxRWR1yRZLlpQN98w==} + + prosemirror-trailing-node@3.0.0: + resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} + peerDependencies: + prosemirror-model: ^1.22.1 + prosemirror-state: ^1.4.2 + prosemirror-view: ^1.33.8 + + prosemirror-transform@1.10.2: + resolution: {integrity: sha512-2iUq0wv2iRoJO/zj5mv8uDUriOHWzXRnOTVgCzSXnktS/2iQRa3UUQwVlkBlYZFtygw6Nh1+X4mGqoYBINn5KQ==} + + prosemirror-view@1.36.0: + resolution: {integrity: sha512-U0GQd5yFvV5qUtT41X1zCQfbw14vkbbKwLlQXhdylEmgpYVHkefXYcC4HHwWOfZa3x6Y8wxDLUBv7dxN5XQ3nA==} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -5110,6 +5546,10 @@ packages: public-encrypt@4.0.3: resolution: {integrity: sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} + punycode@1.4.1: resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} @@ -5213,6 +5653,16 @@ packages: '@types/react': optional: true + react-remove-scroll@2.6.0: + resolution: {integrity: sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + react-style-singleton@2.2.1: resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==} engines: {node: '>=10'} @@ -5350,6 +5800,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rope-sequence@1.3.4: + resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} + rtl-detect@1.1.2: resolution: {integrity: sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==} @@ -5722,6 +6175,15 @@ packages: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tippy.js@6.3.7: + resolution: {integrity: sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==} + + tiptap-extension-auto-joiner@0.1.3: + resolution: {integrity: sha512-nY3aKeCpVb2WjjVEZkLtEqxsK3KU1zGioyglMhK1sUFNjKDccOfRyz/YDKrHRAVsKJPGnk2A8VA1827iGEAXWQ==} + + tiptap-extension-global-drag-handle@0.1.15: + resolution: {integrity: sha512-gpKXzeB4xtg3klhADRqkvoU9F0TCdlDmNtAO5J4SZgxWEfZ8/KNVdPTWlwiKPmOYYrgPnyFd53f6g+mAGoofng==} + to-fast-properties@2.0.0: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} @@ -5828,6 +6290,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} @@ -6008,6 +6473,9 @@ packages: vm-browserify@1.1.2: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} + w3c-keyname@2.2.8: + resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + watchpack@2.4.2: resolution: {integrity: sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==} engines: {node: '>=10.13.0'} @@ -7637,6 +8105,8 @@ snapshots: type-fest: 4.26.1 webpack-hot-middleware: 2.26.1 + '@popperjs/core@2.11.8': {} + '@prisma/client@5.19.1(prisma@5.19.1)': optionalDependencies: prisma: 5.19.1 @@ -7779,12 +8249,46 @@ snapshots: '@types/react': 18.3.5 '@types/react-dom': 18.3.0 + '@radix-ui/react-dismissable-layer@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-use-escape-keydown': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + react: 19.0.0-rc-e740d4b1-20240919 + react-dom: 19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919) + optionalDependencies: + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-dropdown-menu@2.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-menu': 2.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + react: 19.0.0-rc-e740d4b1-20240919 + react-dom: 19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919) + optionalDependencies: + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + '@radix-ui/react-focus-guards@1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919)': dependencies: react: 19.0.0-rc-e740d4b1-20240919 optionalDependencies: '@types/react': 18.3.5 + '@radix-ui/react-focus-guards@1.1.1(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919)': + dependencies: + react: 19.0.0-rc-e740d4b1-20240919 + optionalDependencies: + '@types/react': 18.3.5 + '@radix-ui/react-focus-scope@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) @@ -7812,6 +8316,32 @@ snapshots: '@types/react': 18.3.5 '@types/react-dom': 18.3.0 + '@radix-ui/react-menu@2.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-collection': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-context': 1.1.1(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-dismissable-layer': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-focus-guards': 1.1.1(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-focus-scope': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-id': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-popper': 1.2.0(patch_hash=s4lbvjtshel3lcdrallygm5hhi)(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-portal': 1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-presence': 1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-slot': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + aria-hidden: 1.2.4 + react: 19.0.0-rc-e740d4b1-20240919 + react-dom: 19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919) + react-remove-scroll: 2.6.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + optionalDependencies: + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + '@radix-ui/react-popover@1.1.1(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -7863,6 +8393,16 @@ snapshots: '@types/react': 18.3.5 '@types/react-dom': 18.3.0 + '@radix-ui/react-portal@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-use-layout-effect': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + react: 19.0.0-rc-e740d4b1-20240919 + react-dom: 19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919) + optionalDependencies: + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + '@radix-ui/react-presence@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) @@ -7956,6 +8496,15 @@ snapshots: '@types/react': 18.3.5 '@types/react-dom': 18.3.0 + '@radix-ui/react-separator@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919)': + dependencies: + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + react: 19.0.0-rc-e740d4b1-20240919 + react-dom: 19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919) + optionalDependencies: + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + '@radix-ui/react-slot@1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919)': dependencies: '@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) @@ -7978,6 +8527,47 @@ snapshots: '@types/react': 18.3.5 '@types/react-dom': 18.3.0 + '@radix-ui/react-toggle-group@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-context': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-toggle': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + react: 19.0.0-rc-e740d4b1-20240919 + react-dom: 19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919) + optionalDependencies: + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-toggle@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-use-controllable-state': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + react: 19.0.0-rc-e740d4b1-20240919 + react-dom: 19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919) + optionalDependencies: + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + + '@radix-ui/react-toolbar@1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919)': + dependencies: + '@radix-ui/primitive': 1.1.0 + '@radix-ui/react-context': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-direction': 1.1.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-primitive': 2.0.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-roving-focus': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-separator': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + '@radix-ui/react-toggle-group': 1.1.0(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919) + react: 19.0.0-rc-e740d4b1-20240919 + react-dom: 19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919) + optionalDependencies: + '@types/react': 18.3.5 + '@types/react-dom': 18.3.0 + '@radix-ui/react-tooltip@1.1.2(@types/react-dom@18.3.0)(@types/react@18.3.5)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919)': dependencies: '@radix-ui/primitive': 1.1.0 @@ -8055,6 +8645,8 @@ snapshots: '@radix-ui/rect@1.1.0': {} + '@remirror/core-constants@3.0.0': {} + '@rollup/rollup-android-arm-eabi@4.24.0': optional: true @@ -8610,6 +9202,183 @@ snapshots: dependencies: '@testing-library/dom': 10.4.0 + '@tiptap/core@2.9.1(@tiptap/pm@2.9.1)': + dependencies: + '@tiptap/pm': 2.9.1 + + '@tiptap/extension-blockquote@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-bold@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-bubble-menu@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/pm': 2.9.1 + tippy.js: 6.3.7 + + '@tiptap/extension-bullet-list@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-code-block@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/pm': 2.9.1 + + '@tiptap/extension-code@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-document@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-dropcursor@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/pm': 2.9.1 + + '@tiptap/extension-floating-menu@2.10.3(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/pm': 2.9.1 + tippy.js: 6.3.7 + + '@tiptap/extension-gapcursor@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/pm': 2.9.1 + + '@tiptap/extension-hard-break@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-heading@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-history@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/pm': 2.9.1 + + '@tiptap/extension-horizontal-rule@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/pm': 2.9.1 + + '@tiptap/extension-image@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-italic@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-link@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/pm': 2.9.1 + linkifyjs: 4.1.3 + + '@tiptap/extension-list-item@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-ordered-list@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-paragraph@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-strike@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-text-style@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-text@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-typography@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/extension-underline@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + + '@tiptap/pm@2.9.1': + dependencies: + prosemirror-changeset: 2.2.1 + prosemirror-collab: 1.3.1 + prosemirror-commands: 1.6.2 + prosemirror-dropcursor: 1.8.1 + prosemirror-gapcursor: 1.3.2 + prosemirror-history: 1.4.1 + prosemirror-inputrules: 1.4.0 + prosemirror-keymap: 1.2.2 + prosemirror-markdown: 1.13.1 + prosemirror-menu: 1.2.4 + prosemirror-model: 1.23.0 + prosemirror-schema-basic: 1.2.3 + prosemirror-schema-list: 1.4.1 + prosemirror-state: 1.4.3 + prosemirror-tables: 1.6.1 + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.23.0)(prosemirror-state@1.4.3)(prosemirror-view@1.36.0) + prosemirror-transform: 1.10.2 + prosemirror-view: 1.36.0 + + '@tiptap/react@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)(react-dom@19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919))(react@19.0.0-rc-e740d4b1-20240919)': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/extension-bubble-menu': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) + '@tiptap/extension-floating-menu': 2.10.3(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) + '@tiptap/pm': 2.9.1 + '@types/use-sync-external-store': 0.0.6 + fast-deep-equal: 3.1.3 + react: 19.0.0-rc-e740d4b1-20240919 + react-dom: 19.0.0-rc-e740d4b1-20240919(react@19.0.0-rc-e740d4b1-20240919) + use-sync-external-store: 1.2.2(react@19.0.0-rc-e740d4b1-20240919) + + '@tiptap/starter-kit@2.9.1': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/extension-blockquote': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-bold': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-bullet-list': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-code': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-code-block': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) + '@tiptap/extension-document': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-dropcursor': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) + '@tiptap/extension-gapcursor': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) + '@tiptap/extension-hard-break': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-heading': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-history': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) + '@tiptap/extension-horizontal-rule': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1) + '@tiptap/extension-italic': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-list-item': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-ordered-list': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-paragraph': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-strike': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-text': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/extension-text-style': 2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1)) + '@tiptap/pm': 2.9.1 + + '@tiptap/suggestion@2.9.1(@tiptap/core@2.9.1(@tiptap/pm@2.9.1))(@tiptap/pm@2.9.1)': + dependencies: + '@tiptap/core': 2.9.1(@tiptap/pm@2.9.1) + '@tiptap/pm': 2.9.1 + '@total-typescript/ts-reset@0.5.1': {} '@tybys/wasm-util@0.8.3': @@ -8690,8 +9459,17 @@ snapshots: '@types/json5@0.0.29': {} + '@types/linkify-it@5.0.0': {} + '@types/lodash@4.17.7': {} + '@types/markdown-it@14.1.2': + dependencies: + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 + + '@types/mdurl@2.0.0': {} + '@types/mdx@2.0.13': {} '@types/mime@1.3.5': {} @@ -8742,6 +9520,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/use-sync-external-store@0.0.6': {} + '@types/uuid@9.0.8': {} '@types/validator@13.12.1': {} @@ -9602,6 +10382,8 @@ snapshots: safe-buffer: 5.2.1 sha.js: 2.4.11 + crelt@1.0.6: {} + cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -9848,6 +10630,8 @@ snapshots: entities@2.2.0: {} + entities@4.5.0: {} + env-paths@2.2.1: {} error-ex@1.3.2: @@ -10946,6 +11730,12 @@ snapshots: lines-and-columns@1.2.4: {} + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 + + linkifyjs@4.1.3: {} + loader-runner@4.3.0: {} loader-utils@2.0.4: @@ -11012,6 +11802,15 @@ snapshots: map-or-similar@1.5.0: {} + markdown-it@14.1.0: + dependencies: + argparse: 2.0.1 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 + markdown-to-jsx@7.5.0(react@18.3.1): dependencies: react: 18.3.1 @@ -11026,6 +11825,8 @@ snapshots: inherits: 2.0.4 safe-buffer: 5.2.1 + mdurl@2.0.0: {} + media-typer@0.3.0: {} memfs-browser@3.5.10302: @@ -11268,6 +12069,8 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + orderedmap@2.1.1: {} + os-browserify@0.3.0: {} oslo@1.2.0: @@ -11526,6 +12329,109 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + prosemirror-changeset@2.2.1: + dependencies: + prosemirror-transform: 1.10.2 + + prosemirror-collab@1.3.1: + dependencies: + prosemirror-state: 1.4.3 + + prosemirror-commands@1.6.2: + dependencies: + prosemirror-model: 1.23.0 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + + prosemirror-dropcursor@1.8.1: + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + prosemirror-view: 1.36.0 + + prosemirror-gapcursor@1.3.2: + dependencies: + prosemirror-keymap: 1.2.2 + prosemirror-model: 1.23.0 + prosemirror-state: 1.4.3 + prosemirror-view: 1.36.0 + + prosemirror-history@1.4.1: + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + prosemirror-view: 1.36.0 + rope-sequence: 1.3.4 + + prosemirror-inputrules@1.4.0: + dependencies: + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + + prosemirror-keymap@1.2.2: + dependencies: + prosemirror-state: 1.4.3 + w3c-keyname: 2.2.8 + + prosemirror-markdown@1.13.1: + dependencies: + '@types/markdown-it': 14.1.2 + markdown-it: 14.1.0 + prosemirror-model: 1.23.0 + + prosemirror-menu@1.2.4: + dependencies: + crelt: 1.0.6 + prosemirror-commands: 1.6.2 + prosemirror-history: 1.4.1 + prosemirror-state: 1.4.3 + + prosemirror-model@1.23.0: + dependencies: + orderedmap: 2.1.1 + + prosemirror-schema-basic@1.2.3: + dependencies: + prosemirror-model: 1.23.0 + + prosemirror-schema-list@1.4.1: + dependencies: + prosemirror-model: 1.23.0 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + + prosemirror-state@1.4.3: + dependencies: + prosemirror-model: 1.23.0 + prosemirror-transform: 1.10.2 + prosemirror-view: 1.36.0 + + prosemirror-tables@1.6.1: + dependencies: + prosemirror-keymap: 1.2.2 + prosemirror-model: 1.23.0 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + prosemirror-view: 1.36.0 + + prosemirror-trailing-node@3.0.0(prosemirror-model@1.23.0)(prosemirror-state@1.4.3)(prosemirror-view@1.36.0): + dependencies: + '@remirror/core-constants': 3.0.0 + escape-string-regexp: 4.0.0 + prosemirror-model: 1.23.0 + prosemirror-state: 1.4.3 + prosemirror-view: 1.36.0 + + prosemirror-transform@1.10.2: + dependencies: + prosemirror-model: 1.23.0 + + prosemirror-view@1.36.0: + dependencies: + prosemirror-model: 1.23.0 + prosemirror-state: 1.4.3 + prosemirror-transform: 1.10.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -11540,6 +12446,8 @@ snapshots: randombytes: 2.1.0 safe-buffer: 5.2.1 + punycode.js@2.3.1: {} + punycode@1.4.1: {} punycode@2.3.1: {} @@ -11653,6 +12561,17 @@ snapshots: optionalDependencies: '@types/react': 18.3.5 + react-remove-scroll@2.6.0(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919): + dependencies: + react: 19.0.0-rc-e740d4b1-20240919 + react-remove-scroll-bar: 2.3.6(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + react-style-singleton: 2.2.1(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + tslib: 2.7.0 + use-callback-ref: 1.3.2(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + use-sidecar: 1.1.2(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919) + optionalDependencies: + '@types/react': 18.3.5 + react-style-singleton@2.2.1(@types/react@18.3.5)(react@19.0.0-rc-e740d4b1-20240919): dependencies: get-nonce: 1.0.1 @@ -11847,6 +12766,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.24.0 fsevents: 2.3.3 + rope-sequence@1.3.4: {} + rtl-detect@1.1.2: {} run-parallel@1.2.0: @@ -12290,6 +13211,14 @@ snapshots: tinyspy@3.0.2: {} + tippy.js@6.3.7: + dependencies: + '@popperjs/core': 2.11.8 + + tiptap-extension-auto-joiner@0.1.3: {} + + tiptap-extension-global-drag-handle@0.1.15: {} + to-fast-properties@2.0.0: {} to-regex-range@5.0.1: @@ -12396,6 +13325,8 @@ snapshots: typescript@5.6.2: {} + uc.micro@2.1.0: {} + unbox-primitive@1.0.2: dependencies: call-bind: 1.0.7 @@ -12481,7 +13412,6 @@ snapshots: use-sync-external-store@1.2.2(react@19.0.0-rc-e740d4b1-20240919): dependencies: react: 19.0.0-rc-e740d4b1-20240919 - optional: true util-deprecate@1.0.2: {} @@ -12567,6 +13497,8 @@ snapshots: vm-browserify@1.1.2: {} + w3c-keyname@2.2.8: {} + watchpack@2.4.2: dependencies: glob-to-regexp: 0.4.1 diff --git a/schemas/protocol/variables.ts b/schemas/protocol/variables.ts index 8d8eff1d..92e10bf1 100644 --- a/schemas/protocol/variables.ts +++ b/schemas/protocol/variables.ts @@ -64,6 +64,7 @@ export type TVariableType = z.infer; const BaseVariableDefinitionSchema = z.object({ label: LocalisedStringSchema, validation: VariableValidationSchema.optional(), + control: z.string().optional(), // todo: make this a union of valid control types }); const NormalVariableDefinitionSchema = BaseVariableDefinitionSchema.extend({ diff --git a/styles/global.css b/styles/global.css index edfa4b56..b3470995 100644 --- a/styles/global.css +++ b/styles/global.css @@ -41,3 +41,35 @@ @apply ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2; } } + +.drag-handle { + position: fixed; + opacity: 1; + transition: opacity ease-in 0.2s; + border-radius: 0.25rem; + + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 10' style='fill: rgba(0, 0, 0, 0.5)'%3E%3Cpath d='M3,2 C2.44771525,2 2,1.55228475 2,1 C2,0.44771525 2.44771525,0 3,0 C3.55228475,0 4,0.44771525 4,1 C4,1.55228475 3.55228475,2 3,2 Z M3,6 C2.44771525,6 2,5.55228475 2,5 C2,4.44771525 2.44771525,4 3,4 C3.55228475,4 4,4.44771525 4,5 C4,5.55228475 3.55228475,6 3,6 Z M3,10 C2.44771525,10 2,9.55228475 2,9 C2,8.44771525 2.44771525,8 3,8 C3.55228475,8 4,8.44771525 4,9 C4,9.55228475 3.55228475,10 3,10 Z M7,2 C6.44771525,2 6,1.55228475 6,1 C6,0.44771525 6.44771525,0 7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 Z M7,6 C6.44771525,6 6,5.55228475 6,5 C6,4.44771525 6.44771525,4 7,4 C7.55228475,4 8,4.44771525 8,5 C8,5.55228475 7.55228475,6 7,6 Z M7,10 C6.44771525,10 6,9.55228475 6,9 C6,8.44771525 6.44771525,8 7,8 C7.55228475,8 8,8.44771525 8,9 C8,9.55228475 7.55228475,10 7,10 Z'%3E%3C/path%3E%3C/svg%3E"); + background-size: calc(0.5em + 0.375rem) calc(0.5em + 0.375rem); + background-repeat: no-repeat; + background-position: center; + width: 1.2rem; + height: 1.5rem; + z-index: 50; + cursor: grab; + + &:hover { + background-color: red; + transition: background-color 0.2s; + } + + &:active { + background-color: green; + transition: background-color 0.2s; + cursor: grabbing; + } + + &.hide { + opacity: 0; + pointer-events: none; + } +}
+ This is a paragraph block. +
+ Text following h2. +