diff --git a/package.json b/package.json index 6f7930a..652ec63 100644 --- a/package.json +++ b/package.json @@ -79,8 +79,10 @@ "clsx": "^2.1.1", "cmdk": "^1.0.4", "date-fns": "^4.1.0", + "html2canvas": "^1.4.1", "lucide-react": "0.456.0", "next": "^15.0.3", + "pdf-lib": "^1.17.1", "prismjs": "^1.29.0", "react": "^18.3.1", "react-day-picker": "8.10.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94d988f..858dd81 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -206,12 +206,18 @@ importers: date-fns: specifier: ^4.1.0 version: 4.1.0 + html2canvas: + specifier: ^1.4.1 + version: 1.4.1 lucide-react: specifier: 0.456.0 version: 0.456.0(react@18.3.1) next: specifier: ^15.0.3 version: 15.0.3(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + pdf-lib: + specifier: ^1.17.1 + version: 1.17.1 prismjs: specifier: ^1.29.0 version: 1.29.0 @@ -884,6 +890,12 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} + '@pdf-lib/standard-fonts@1.0.0': + resolution: {integrity: sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==} + + '@pdf-lib/upng@1.0.1': + resolution: {integrity: sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -2334,6 +2346,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -2462,6 +2478,9 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + css-line-break@2.1.0: + resolution: {integrity: sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==} + css-select@4.3.0: resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} @@ -3032,6 +3051,10 @@ packages: hoist-non-react-statics@3.3.2: resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + html2canvas@1.4.1: + resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} + engines: {node: '>=8.0.0'} + htmlparser2@5.0.1: resolution: {integrity: sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==} @@ -3627,6 +3650,9 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -3660,6 +3686,9 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pdf-lib@1.17.1: + resolution: {integrity: sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==} + performance-now@2.1.0: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} @@ -4180,6 +4209,9 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} + text-segmentation@1.0.3: + resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} @@ -4225,6 +4257,9 @@ packages: tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -4388,6 +4423,9 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + utrie@1.0.2: + resolution: {integrity: sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==} + valid-data-url@3.0.1: resolution: {integrity: sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==} engines: {node: '>=10'} @@ -4914,6 +4952,14 @@ snapshots: '@opentelemetry/api@1.9.0': {} + '@pdf-lib/standard-fonts@1.0.0': + dependencies: + pako: 1.0.11 + + '@pdf-lib/upng@1.0.1': + dependencies: + pako: 1.0.11 + '@pkgjs/parseargs@0.11.0': optional: true @@ -6518,6 +6564,8 @@ snapshots: balanced-match@1.0.2: {} + base64-arraybuffer@1.0.2: {} + binary-extensions@2.3.0: {} boolbase@1.0.0: {} @@ -6663,6 +6711,10 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + css-line-break@2.1.0: + dependencies: + utrie: 1.0.2 + css-select@4.3.0: dependencies: boolbase: 1.0.0 @@ -7385,6 +7437,11 @@ snapshots: dependencies: react-is: 16.13.1 + html2canvas@1.4.1: + dependencies: + css-line-break: 2.1.0 + text-segmentation: 1.0.3 + htmlparser2@5.0.1: dependencies: domelementtype: 2.3.0 @@ -8124,6 +8181,8 @@ snapshots: package-json-from-dist@1.0.1: {} + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -8149,6 +8208,13 @@ snapshots: path-type@4.0.0: {} + pdf-lib@1.17.1: + dependencies: + '@pdf-lib/standard-fonts': 1.0.0 + '@pdf-lib/upng': 1.0.1 + pako: 1.0.11 + tslib: 1.14.1 + performance-now@2.1.0: {} picocolors@1.1.0: {} @@ -8760,6 +8826,10 @@ snapshots: tapable@2.2.1: {} + text-segmentation@1.0.3: + dependencies: + utrie: 1.0.2 + text-table@0.2.0: {} thenify-all@1.6.0: @@ -8799,6 +8869,8 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 + tslib@1.14.1: {} + tslib@2.8.1: {} tsx@4.19.2: @@ -8961,6 +9033,10 @@ snapshots: util-deprecate@1.0.2: {} + utrie@1.0.2: + dependencies: + base64-arraybuffer: 1.0.2 + valid-data-url@3.0.1: {} validator@13.12.0: {} diff --git a/src/components/plate-ui/button.tsx b/src/components/plate-ui/button.tsx index d017db4..a4be477 100644 --- a/src/components/plate-ui/button.tsx +++ b/src/components/plate-ui/button.tsx @@ -17,7 +17,7 @@ export const buttonVariants = cva( }, size: { icon: 'size-[28px] rounded-md px-1.5', - lg: 'h-10 rounded-md px-4', + lg: 'h-9 rounded-md px-4', md: 'h-8 px-3 text-sm', none: '', sm: 'h-[28px] rounded-md px-2.5', diff --git a/src/components/plate-ui/editor.tsx b/src/components/plate-ui/editor.tsx index 6810bb1..093b79d 100644 --- a/src/components/plate-ui/editor.tsx +++ b/src/components/plate-ui/editor.tsx @@ -46,7 +46,6 @@ export const EditorContainer = ({ editorContainerVariants({ variant }), className )} - role="button" {...props} /> ); @@ -75,9 +74,9 @@ const editorVariants = cva( true: 'ring-2 ring-ring ring-offset-2', }, variant: { - ai: 'w-full px-0 text-sm', + ai: 'w-full px-0 text-base md:text-sm', aiChat: - 'max-h-[min(70vh,320px)] w-full max-w-[700px] overflow-y-auto px-3 py-2 text-sm', + 'max-h-[min(70vh,320px)] w-full max-w-[700px] overflow-y-auto px-3 py-2 text-base md:text-sm', default: 'size-full px-16 pb-72 pt-4 text-base sm:px-[max(64px,calc(50%-350px))]', demo: 'size-full px-16 pb-72 pt-4 text-base sm:px-[max(64px,calc(50%-350px))]', diff --git a/src/components/plate-ui/export-toolbar-button.tsx b/src/components/plate-ui/export-toolbar-button.tsx new file mode 100644 index 0000000..a659a8b --- /dev/null +++ b/src/components/plate-ui/export-toolbar-button.tsx @@ -0,0 +1,93 @@ +'use client'; + +import React from 'react'; + +import type { DropdownMenuProps } from '@radix-ui/react-dropdown-menu'; + +import { toDOMNode, useEditorRef } from '@udecode/plate-common/react'; +import { ArrowDownToLineIcon } from 'lucide-react'; + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuTrigger, + useOpenState, +} from './dropdown-menu'; +import { ToolbarButton } from './toolbar'; + +export function ExportToolbarButton({ children, ...props }: DropdownMenuProps) { + const editor = useEditorRef(); + const openState = useOpenState(); + + const getCanvas = async () => { + const { default: html2canvas } = await import('html2canvas'); + + const style = document.createElement('style'); + document.head.append(style); + style.sheet?.insertRule( + 'body > div:last-child img { display: inline-block !important; }' + ); + + const canvas = await html2canvas(toDOMNode(editor, editor)!); + style.remove(); + + return canvas; + }; + + const downloadFile = (href: string, filename: string) => { + const element = document.createElement('a'); + element.setAttribute('href', href); + element.setAttribute('download', filename); + element.style.display = 'none'; + document.body.append(element); + element.click(); + element.remove(); + }; + + const exportToPdf = async () => { + const canvas = await getCanvas(); + + const PDFLib = await import('pdf-lib'); + const pdfDoc = await PDFLib.PDFDocument.create(); + const page = pdfDoc.addPage([canvas.width, canvas.height]); + const imageEmbed = await pdfDoc.embedPng(canvas.toDataURL('PNG')); + const { height, width } = imageEmbed.scale(1); + page.drawImage(imageEmbed, { + height, + width, + x: 0, + y: 0, + }); + const pdfBase64 = await pdfDoc.saveAsBase64({ dataUri: true }); + + downloadFile(pdfBase64, 'plate.pdf'); + }; + + const exportToImage = async () => { + const canvas = await getCanvas(); + downloadFile(canvas.toDataURL('image/png'), 'plate.png'); + }; + + return ( + + + + + + + + + + + Export as PDF + + + Export as Image + + + + + ); +} diff --git a/src/components/plate-ui/fixed-toolbar-buttons.tsx b/src/components/plate-ui/fixed-toolbar-buttons.tsx index 4b26dcf..9d93e84 100644 --- a/src/components/plate-ui/fixed-toolbar-buttons.tsx +++ b/src/components/plate-ui/fixed-toolbar-buttons.tsx @@ -23,6 +23,7 @@ import { VideoPlugin, } from '@udecode/plate-media/react'; import { + ArrowUpToLineIcon, BaselineIcon, BoldIcon, Code2Icon, @@ -41,6 +42,7 @@ import { AlignDropdownMenu } from './align-dropdown-menu'; import { ColorDropdownMenu } from './color-dropdown-menu'; import { CommentToolbarButton } from './comment-toolbar-button'; import { EmojiDropdownMenu } from './emoji-dropdown-menu'; +import { ExportToolbarButton } from './export-toolbar-button'; import { RedoToolbarButton, UndoToolbarButton } from './history-toolbar-button'; import { IndentListToolbarButton } from './indent-list-toolbar-button'; import { IndentTodoToolbarButton } from './indent-todo-toolbar-button'; @@ -75,6 +77,12 @@ export function FixedToolbarButtons() { + + + + + + diff --git a/src/components/plate-ui/input.tsx b/src/components/plate-ui/input.tsx index ee0a423..ce7a709 100644 --- a/src/components/plate-ui/input.tsx +++ b/src/components/plate-ui/input.tsx @@ -4,7 +4,7 @@ import { cn, withVariants } from '@udecode/cn'; import { type VariantProps, cva } from 'class-variance-authority'; export const inputVariants = cva( - 'flex w-full rounded-md bg-transparent text-sm file:border-0 file:bg-background file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50', + 'flex w-full rounded-md bg-transparent text-base file:border-0 file:bg-background file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm', { defaultVariants: { h: 'md', diff --git a/src/components/plate-ui/media-audio-element.tsx b/src/components/plate-ui/media-audio-element.tsx index fa0b95a..c300012 100644 --- a/src/components/plate-ui/media-audio-element.tsx +++ b/src/components/plate-ui/media-audio-element.tsx @@ -24,7 +24,6 @@ export const MediaAudioElement = withHOC( >
- {/* eslint-disable-next-line jsx-a11y/media-has-caption */}
diff --git a/src/components/plate-ui/toc-element.tsx b/src/components/plate-ui/toc-element.tsx index 844b2be..a282c8d 100644 --- a/src/components/plate-ui/toc-element.tsx +++ b/src/components/plate-ui/toc-element.tsx @@ -37,7 +37,7 @@ export const TocElement = withRef( className={cn('relative mb-1 p-0', className)} {...props} > - + {children} ); diff --git a/src/components/plate-ui/toolbar.tsx b/src/components/plate-ui/toolbar.tsx index 9200d1d..1d27f55 100644 --- a/src/components/plate-ui/toolbar.tsx +++ b/src/components/plate-ui/toolbar.tsx @@ -81,7 +81,6 @@ const dropdownArrowVariants = cva( ); const ToolbarButton = withTooltip( - // eslint-disable-next-line react/display-name React.forwardRef< React.ElementRef, {