PDF editor example (#3159)
This diff adds a PDF editor example. It's pretty similar to the image annotator, but is a better way to demo longer axis-locked scrolling. There are some pretty big drawbacks to it at the moment (see the TODO list on `PdfEditor.tsx`) I'm going to land as-is for now, and we can iterate on it in the future. ### Change Type - [x] `docs` — Changes to the documentation, examples, or templates. - [x] `feature` — New feature
This commit is contained in:
parent
3a736007e5
commit
4c5c3daa51
13 changed files with 688 additions and 1 deletions
|
@ -11,6 +11,8 @@ const examplesWithoutCanvas = [
|
|||
'yjs',
|
||||
// starts by asking the user to select an image
|
||||
'image-annotator',
|
||||
// starts by asking the user to open a pdf
|
||||
'pdf-editor',
|
||||
]
|
||||
const exampelsToTest = examplesFolderList.filter((route) => !examplesWithoutCanvas.includes(route))
|
||||
|
||||
|
|
|
@ -40,6 +40,8 @@
|
|||
"classnames": "^2.3.2",
|
||||
"lazyrepo": "0.0.0-alpha.27",
|
||||
"lodash": "^4.17.21",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"pdfjs-dist": "^4.0.379",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.17.0",
|
||||
|
|
|
@ -22,6 +22,7 @@ import { AnnotatorImage } from './types'
|
|||
// TODO:
|
||||
// - prevent changing pages (create page, change page, move shapes to new page)
|
||||
// - prevent locked shape context menu
|
||||
// - inertial scrolling for constrained camera
|
||||
export function ImageAnnotationEditor({
|
||||
image,
|
||||
onDone,
|
||||
|
|
89
apps/examples/src/examples/pdf-editor/ExportPdfButton.tsx
Normal file
89
apps/examples/src/examples/pdf-editor/ExportPdfButton.tsx
Normal file
|
@ -0,0 +1,89 @@
|
|||
import { PDFDocument } from 'pdf-lib'
|
||||
import { useState } from 'react'
|
||||
import { Editor, assert, exportToBlob, useEditor } from 'tldraw'
|
||||
import { Pdf } from './PdfPicker'
|
||||
|
||||
export function ExportPdfButton({ pdf }: { pdf: Pdf }) {
|
||||
const [exportProgress, setExportProgress] = useState<number | null>(null)
|
||||
const editor = useEditor()
|
||||
|
||||
return (
|
||||
<button
|
||||
className="ExportPdfButton"
|
||||
onClick={async () => {
|
||||
setExportProgress(0)
|
||||
try {
|
||||
await exportPdf(editor, pdf, setExportProgress)
|
||||
} finally {
|
||||
setExportProgress(null)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{exportProgress ? `Exporting... ${Math.round(exportProgress * 100)}%` : 'Export PDF'}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
async function exportPdf(
|
||||
editor: Editor,
|
||||
{ name, source, pages }: Pdf,
|
||||
onProgress: (progress: number) => void
|
||||
) {
|
||||
const totalThings = pages.length * 2 + 2
|
||||
let progressCount = 0
|
||||
const tickProgress = () => {
|
||||
progressCount++
|
||||
onProgress(progressCount / totalThings)
|
||||
}
|
||||
|
||||
const pdf = await PDFDocument.load(source)
|
||||
tickProgress()
|
||||
|
||||
const pdfPages = pdf.getPages()
|
||||
assert(pdfPages.length === pages.length, 'PDF page count mismatch')
|
||||
|
||||
const pageShapeIds = new Set(pages.map((page) => page.shapeId))
|
||||
const allIds = Array.from(editor.getCurrentPageShapeIds()).filter((id) => !pageShapeIds.has(id))
|
||||
|
||||
for (let i = 0; i < pages.length; i++) {
|
||||
const page = pages[i]
|
||||
const pdfPage = pdfPages[i]
|
||||
|
||||
const bounds = page.bounds
|
||||
const shapesInBounds = allIds.filter((id) => {
|
||||
const shapePageBounds = editor.getShapePageBounds(id)
|
||||
if (!shapePageBounds) return false
|
||||
return shapePageBounds.collides(bounds)
|
||||
})
|
||||
|
||||
if (shapesInBounds.length === 0) {
|
||||
tickProgress()
|
||||
tickProgress()
|
||||
continue
|
||||
}
|
||||
|
||||
const exportedPng = await exportToBlob({
|
||||
editor,
|
||||
ids: allIds,
|
||||
format: 'png',
|
||||
opts: { background: false, bounds: page.bounds, padding: 0, scale: 2 },
|
||||
})
|
||||
tickProgress()
|
||||
|
||||
pdfPage.drawImage(await pdf.embedPng(await exportedPng.arrayBuffer()), {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: pdfPage.getWidth(),
|
||||
height: pdfPage.getHeight(),
|
||||
})
|
||||
tickProgress()
|
||||
}
|
||||
|
||||
const url = URL.createObjectURL(new Blob([await pdf.save()], { type: 'application/pdf' }))
|
||||
tickProgress()
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = name
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
289
apps/examples/src/examples/pdf-editor/PdfEditor.tsx
Normal file
289
apps/examples/src/examples/pdf-editor/PdfEditor.tsx
Normal file
|
@ -0,0 +1,289 @@
|
|||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import {
|
||||
Box,
|
||||
SVGContainer,
|
||||
TLImageShape,
|
||||
TLShapeId,
|
||||
TLShapePartial,
|
||||
Tldraw,
|
||||
clamp,
|
||||
compact,
|
||||
getIndicesBetween,
|
||||
react,
|
||||
sortByIndex,
|
||||
track,
|
||||
useBreakpoint,
|
||||
useEditor,
|
||||
} from 'tldraw'
|
||||
import { PORTRAIT_BREAKPOINT } from 'tldraw/src/lib/ui/constants'
|
||||
import { ExportPdfButton } from './ExportPdfButton'
|
||||
import { Pdf } from './PdfPicker'
|
||||
|
||||
// TODO:
|
||||
// - prevent changing pages (create page, change page, move shapes to new page)
|
||||
// - prevent locked shape context menu
|
||||
// - inertial scrolling for constrained camera
|
||||
// - render pages on-demand instead of all at once.
|
||||
export function PdfEditor({ pdf }: { pdf: Pdf }) {
|
||||
const pdfShapeIds = useMemo(() => pdf.pages.map((page) => page.shapeId), [pdf.pages])
|
||||
return (
|
||||
<Tldraw
|
||||
onMount={(editor) => {
|
||||
editor.updateInstanceState({ isDebugMode: false })
|
||||
editor.setCamera({ x: 1000, y: 1000, z: 1 })
|
||||
|
||||
editor.createAssets(
|
||||
pdf.pages.map((page) => ({
|
||||
id: page.assetId,
|
||||
typeName: 'asset',
|
||||
type: 'image',
|
||||
meta: {},
|
||||
props: {
|
||||
w: page.bounds.w,
|
||||
h: page.bounds.h,
|
||||
mimeType: 'image/png',
|
||||
src: page.src,
|
||||
name: 'page',
|
||||
isAnimated: false,
|
||||
},
|
||||
}))
|
||||
)
|
||||
|
||||
editor.createShapes(
|
||||
pdf.pages.map(
|
||||
(page): TLShapePartial<TLImageShape> => ({
|
||||
id: page.shapeId,
|
||||
type: 'image',
|
||||
x: page.bounds.x,
|
||||
y: page.bounds.y,
|
||||
props: {
|
||||
assetId: page.assetId,
|
||||
w: page.bounds.w,
|
||||
h: page.bounds.h,
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
}}
|
||||
components={{
|
||||
PageMenu: null,
|
||||
InFrontOfTheCanvas: useCallback(() => {
|
||||
return <PageOverlayScreen pdf={pdf} />
|
||||
}, [pdf]),
|
||||
SharePanel: useCallback(() => {
|
||||
return <ExportPdfButton pdf={pdf} />
|
||||
}, [pdf]),
|
||||
}}
|
||||
>
|
||||
<ConstrainCamera pdf={pdf} />
|
||||
<KeepShapesLocked shapeIds={pdfShapeIds} />
|
||||
<KeepShapesAtBottomOfCurrentPage shapeIds={pdfShapeIds} />
|
||||
</Tldraw>
|
||||
)
|
||||
}
|
||||
|
||||
const PageOverlayScreen = track(function PageOverlayScreen({ pdf }: { pdf: Pdf }) {
|
||||
const editor = useEditor()
|
||||
|
||||
const viewportPageBounds = editor.getViewportPageBounds()
|
||||
const viewportScreenBounds = editor.getViewportScreenBounds()
|
||||
|
||||
const relevantPageBounds = compact(
|
||||
pdf.pages.map((page) => {
|
||||
if (!viewportPageBounds.collides(page.bounds)) return null
|
||||
const topLeft = editor.pageToViewport(page.bounds)
|
||||
const bottomRight = editor.pageToViewport({ x: page.bounds.maxX, y: page.bounds.maxY })
|
||||
return new Box(topLeft.x, topLeft.y, bottomRight.x - topLeft.x, bottomRight.y - topLeft.y)
|
||||
})
|
||||
)
|
||||
|
||||
function pathForPageBounds(bounds: Box) {
|
||||
return `M ${bounds.x} ${bounds.y} L ${bounds.maxX} ${bounds.y} L ${bounds.maxX} ${bounds.maxY} L ${bounds.x} ${bounds.maxY} Z`
|
||||
}
|
||||
|
||||
const viewportPath = `M 0 0 L ${viewportScreenBounds.w} 0 L ${viewportScreenBounds.w} ${viewportScreenBounds.h} L 0 ${viewportScreenBounds.h} Z`
|
||||
|
||||
return (
|
||||
<>
|
||||
<SVGContainer className="PageOverlayScreen-screen">
|
||||
<path
|
||||
d={`${viewportPath} ${relevantPageBounds.map(pathForPageBounds).join(' ')}`}
|
||||
fillRule="evenodd"
|
||||
/>
|
||||
</SVGContainer>
|
||||
{relevantPageBounds.map((bounds, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="PageOverlayScreen-outline"
|
||||
style={{
|
||||
width: bounds.w,
|
||||
height: bounds.h,
|
||||
transform: `translate(${bounds.x}px, ${bounds.y}px)`,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
||||
function ConstrainCamera({ pdf }: { pdf: Pdf }) {
|
||||
const editor = useEditor()
|
||||
const breakpoint = useBreakpoint()
|
||||
const isMobile = breakpoint < PORTRAIT_BREAKPOINT.TABLET_SM
|
||||
|
||||
useEffect(() => {
|
||||
const marginTop = 64
|
||||
const marginSide = isMobile ? 16 : 164
|
||||
const marginBottom = 80
|
||||
|
||||
const targetBounds = pdf.pages.reduce(
|
||||
(acc, page) => acc.union(page.bounds),
|
||||
pdf.pages[0].bounds.clone()
|
||||
)
|
||||
|
||||
function constrainCamera(camera: { x: number; y: number; z: number }): {
|
||||
x: number
|
||||
y: number
|
||||
z: number
|
||||
} {
|
||||
const viewportBounds = editor.getViewportScreenBounds()
|
||||
|
||||
const usableViewport = new Box(
|
||||
marginSide,
|
||||
marginTop,
|
||||
viewportBounds.w - marginSide * 2,
|
||||
viewportBounds.h - marginTop - marginBottom
|
||||
)
|
||||
|
||||
const minZoom = Math.min(
|
||||
usableViewport.w / targetBounds.w,
|
||||
usableViewport.h / targetBounds.h,
|
||||
1
|
||||
)
|
||||
const zoom = Math.max(minZoom, camera.z)
|
||||
|
||||
const centerX = targetBounds.x - targetBounds.w / 2 + usableViewport.midX / zoom
|
||||
const centerY = targetBounds.y - targetBounds.h / 2 + usableViewport.midY / zoom
|
||||
|
||||
const availableXMovement = Math.max(0, targetBounds.w - usableViewport.w / zoom)
|
||||
const availableYMovement = Math.max(0, targetBounds.h - usableViewport.h / zoom)
|
||||
|
||||
return {
|
||||
x: clamp(camera.x, centerX - availableXMovement / 2, centerX + availableXMovement / 2),
|
||||
y: clamp(camera.y, centerY - availableYMovement / 2, centerY + availableYMovement / 2),
|
||||
z: zoom,
|
||||
}
|
||||
}
|
||||
|
||||
const removeOnChange = editor.sideEffects.registerBeforeChangeHandler(
|
||||
'camera',
|
||||
(_prev, next) => {
|
||||
const constrained = constrainCamera(next)
|
||||
if (constrained.x === next.x && constrained.y === next.y && constrained.z === next.z)
|
||||
return next
|
||||
return { ...next, ...constrained }
|
||||
}
|
||||
)
|
||||
|
||||
const removeReaction = react('update camera when viewport/shape changes', () => {
|
||||
const original = editor.getCamera()
|
||||
const constrained = constrainCamera(original)
|
||||
if (
|
||||
original.x === constrained.x &&
|
||||
original.y === constrained.y &&
|
||||
original.z === constrained.z
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
// this needs to be in a microtask for some reason, but idk why
|
||||
queueMicrotask(() => editor.setCamera(constrained))
|
||||
})
|
||||
|
||||
return () => {
|
||||
removeOnChange()
|
||||
removeReaction()
|
||||
}
|
||||
}, [editor, isMobile, pdf.pages])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function KeepShapesLocked({ shapeIds }: { shapeIds: TLShapeId[] }) {
|
||||
const editor = useEditor()
|
||||
|
||||
useEffect(() => {
|
||||
const shapeIdSet = new Set(shapeIds)
|
||||
|
||||
for (const shapeId of shapeIdSet) {
|
||||
const shape = editor.getShape(shapeId)!
|
||||
editor.updateShape({
|
||||
id: shape.id,
|
||||
type: shape.type,
|
||||
isLocked: true,
|
||||
})
|
||||
}
|
||||
|
||||
const removeOnChange = editor.sideEffects.registerBeforeChangeHandler('shape', (prev, next) => {
|
||||
if (!shapeIdSet.has(next.id)) return next
|
||||
if (next.isLocked) return next
|
||||
return { ...prev, isLocked: true }
|
||||
})
|
||||
|
||||
return () => {
|
||||
removeOnChange()
|
||||
}
|
||||
}, [editor, shapeIds])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function KeepShapesAtBottomOfCurrentPage({ shapeIds }: { shapeIds: TLShapeId[] }) {
|
||||
const editor = useEditor()
|
||||
|
||||
useEffect(() => {
|
||||
const shapeIdSet = new Set(shapeIds)
|
||||
|
||||
function makeSureShapesAreAtBottom() {
|
||||
const shapes = shapeIds.map((id) => editor.getShape(id)!).sort(sortByIndex)
|
||||
const pageId = editor.getCurrentPageId()
|
||||
|
||||
const siblings = editor.getSortedChildIdsForParent(pageId)
|
||||
const currentBottomShapes = siblings.slice(0, shapes.length).map((id) => editor.getShape(id)!)
|
||||
|
||||
if (currentBottomShapes.every((shape, i) => shape.id === shapes[i].id)) return
|
||||
|
||||
const otherSiblings = siblings.filter((id) => !shapeIdSet.has(id))
|
||||
const bottomSibling = otherSiblings[0]
|
||||
const lowestIndex = editor.getShape(bottomSibling)!.index
|
||||
|
||||
const indexes = getIndicesBetween(undefined, lowestIndex, shapes.length)
|
||||
editor.updateShapes(
|
||||
shapes.map((shape, i) => ({
|
||||
id: shape.id,
|
||||
type: shape.type,
|
||||
isLocked: shape.isLocked,
|
||||
index: indexes[i],
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
makeSureShapesAreAtBottom()
|
||||
|
||||
const removeOnCreate = editor.sideEffects.registerAfterCreateHandler(
|
||||
'shape',
|
||||
makeSureShapesAreAtBottom
|
||||
)
|
||||
const removeOnChange = editor.sideEffects.registerAfterChangeHandler(
|
||||
'shape',
|
||||
makeSureShapesAreAtBottom
|
||||
)
|
||||
|
||||
return () => {
|
||||
removeOnCreate()
|
||||
removeOnChange()
|
||||
}
|
||||
}, [editor, shapeIds])
|
||||
|
||||
return null
|
||||
}
|
32
apps/examples/src/examples/pdf-editor/PdfEditorExample.tsx
Normal file
32
apps/examples/src/examples/pdf-editor/PdfEditorExample.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { useState } from 'react'
|
||||
import { PdfEditor } from './PdfEditor'
|
||||
import { Pdf, PdfPicker } from './PdfPicker'
|
||||
import './pdf-editor.css'
|
||||
|
||||
type State =
|
||||
| {
|
||||
phase: 'pick'
|
||||
}
|
||||
| {
|
||||
phase: 'edit'
|
||||
pdf: Pdf
|
||||
}
|
||||
|
||||
export default function PdfEditorWrapper() {
|
||||
const [state, setState] = useState<State>({ phase: 'pick' })
|
||||
|
||||
switch (state.phase) {
|
||||
case 'pick':
|
||||
return (
|
||||
<div className="PdfEditor">
|
||||
<PdfPicker onOpenPdf={(pdf) => setState({ phase: 'edit', pdf })} />
|
||||
</div>
|
||||
)
|
||||
case 'edit':
|
||||
return (
|
||||
<div className="PdfEditor">
|
||||
<PdfEditor pdf={state.pdf} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
118
apps/examples/src/examples/pdf-editor/PdfPicker.tsx
Normal file
118
apps/examples/src/examples/pdf-editor/PdfPicker.tsx
Normal file
|
@ -0,0 +1,118 @@
|
|||
import { useState } from 'react'
|
||||
import { AssetRecordType, Box, TLAssetId, TLShapeId, assertExists, createShapeId } from 'tldraw'
|
||||
import tldrawPdf from './assets/tldraw.pdf'
|
||||
|
||||
export type PdfPage = {
|
||||
src: string
|
||||
bounds: Box
|
||||
assetId: TLAssetId
|
||||
shapeId: TLShapeId
|
||||
}
|
||||
|
||||
export type Pdf = {
|
||||
name: string
|
||||
pages: PdfPage[]
|
||||
source: string | ArrayBuffer
|
||||
}
|
||||
|
||||
const pageSpacing = 32
|
||||
|
||||
export function PdfPicker({ onOpenPdf }: { onOpenPdf: (pdf: Pdf) => void }) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
async function loadPdf(name: string, source: ArrayBuffer): Promise<Pdf> {
|
||||
const PdfJS = await import('pdfjs-dist')
|
||||
PdfJS.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.min.mjs',
|
||||
import.meta.url
|
||||
).toString()
|
||||
const pdf = await PdfJS.getDocument(source.slice(0)).promise
|
||||
const pages: PdfPage[] = []
|
||||
|
||||
const canvas = window.document.createElement('canvas')
|
||||
const context = assertExists(canvas.getContext('2d'))
|
||||
|
||||
const visualScale = 1.5
|
||||
const scale = window.devicePixelRatio
|
||||
|
||||
let top = 0
|
||||
let widest = 0
|
||||
for (let i = 1; i <= pdf.numPages; i++) {
|
||||
const page = await pdf.getPage(i)
|
||||
const viewport = page.getViewport({ scale: scale * visualScale })
|
||||
canvas.width = viewport.width
|
||||
canvas.height = viewport.height
|
||||
const renderContext = {
|
||||
canvasContext: context,
|
||||
viewport,
|
||||
}
|
||||
await page.render(renderContext).promise
|
||||
|
||||
const width = viewport.width / scale
|
||||
const height = viewport.height / scale
|
||||
pages.push({
|
||||
src: canvas.toDataURL(),
|
||||
bounds: new Box(0, top, width, height),
|
||||
assetId: AssetRecordType.createId(),
|
||||
shapeId: createShapeId(),
|
||||
})
|
||||
top += height + pageSpacing
|
||||
widest = Math.max(widest, width)
|
||||
}
|
||||
canvas.width = 0
|
||||
canvas.height = 0
|
||||
|
||||
for (const page of pages) {
|
||||
page.bounds.x = (widest - page.bounds.width) / 2
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
pages,
|
||||
source,
|
||||
}
|
||||
}
|
||||
|
||||
function onClickOpenPdf() {
|
||||
const input = window.document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'application/pdf'
|
||||
input.addEventListener('change', async (e) => {
|
||||
const fileList = (e.target as HTMLInputElement).files
|
||||
if (!fileList || fileList.length === 0) return
|
||||
const file = fileList[0]
|
||||
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const pdf = await loadPdf(file.name, await file.arrayBuffer())
|
||||
onOpenPdf(pdf)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
})
|
||||
input.click()
|
||||
}
|
||||
|
||||
async function onClickUseExample() {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
const result = await fetch(tldrawPdf)
|
||||
const pdf = await loadPdf('tldraw.pdf', await result.arrayBuffer())
|
||||
onOpenPdf(pdf)
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="PdfPicker">Loading...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="PdfPicker">
|
||||
<button onClick={onClickOpenPdf}>Open PDF</button>
|
||||
<div>or</div>
|
||||
<button onClick={onClickUseExample}>Use an example</button>
|
||||
</div>
|
||||
)
|
||||
}
|
8
apps/examples/src/examples/pdf-editor/README.md
Normal file
8
apps/examples/src/examples/pdf-editor/README.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
title: PDF editor
|
||||
component: ./PdfEditorExample.tsx
|
||||
category: use-cases
|
||||
priority: 1
|
||||
---
|
||||
|
||||
A very basic PDF editor built with tldraw
|
BIN
apps/examples/src/examples/pdf-editor/assets/tldraw.pdf
Normal file
BIN
apps/examples/src/examples/pdf-editor/assets/tldraw.pdf
Normal file
Binary file not shown.
66
apps/examples/src/examples/pdf-editor/pdf-editor.css
Normal file
66
apps/examples/src/examples/pdf-editor/pdf-editor.css
Normal file
|
@ -0,0 +1,66 @@
|
|||
.PdfEditor {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.PdfEditor .PdfPicker {
|
||||
position: absolute;
|
||||
inset: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
.PdfEditor .PdfPicker button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
background: #eee;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
.PdfEditor .PdfPicker button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.PdfEditor .PdfBgRenderer {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
}
|
||||
.PdfEditor .PdfBgRenderer img {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.PdfEditor .PageOverlayScreen-screen {
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
fill: var(--color-background);
|
||||
fill-opacity: 0.8;
|
||||
stroke: none;
|
||||
}
|
||||
.PdfEditor .PageOverlayScreen-outline {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
/* border: 1px solid var(--color-overlay); */
|
||||
box-shadow: var(--shadow-2);
|
||||
}
|
||||
.PdfEditor .ExportPdfButton {
|
||||
font: inherit;
|
||||
background: var(--color-primary);
|
||||
border: none;
|
||||
color: var(--color-selected-contrast);
|
||||
font-size: 1rem;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
margin: 6px;
|
||||
margin-bottom: 0;
|
||||
pointer-events: all;
|
||||
z-index: var(--layer-panels);
|
||||
border: 2px solid var(--color-background);
|
||||
cursor: pointer;
|
||||
}
|
||||
.PdfEditor .ExportPdfButton:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
|
@ -9,6 +9,10 @@ export default defineConfig({
|
|||
build: {
|
||||
outDir: path.join(__dirname, 'dist'),
|
||||
assetsInlineLimit: 0,
|
||||
target: 'es2022',
|
||||
},
|
||||
esbuild: {
|
||||
target: 'es2022',
|
||||
},
|
||||
server: {
|
||||
port: 5420,
|
||||
|
@ -16,6 +20,9 @@ export default defineConfig({
|
|||
clearScreen: false,
|
||||
optimizeDeps: {
|
||||
exclude: ['@tldraw/assets'],
|
||||
esbuildOptions: {
|
||||
target: 'es2022',
|
||||
},
|
||||
},
|
||||
define: {
|
||||
'process.env.TLDRAW_ENV': JSON.stringify(process.env.VERCEL_ENV ?? 'development'),
|
||||
|
|
|
@ -104,10 +104,15 @@
|
|||
"typescript": "^5.3.3",
|
||||
"vercel": "^28.16.15"
|
||||
},
|
||||
"// resolutions.canvas": [
|
||||
"our examples app depenends on pdf.js which pulls in canvas as an optional dependency.",
|
||||
"it slows down installs quite a bit though, so we replace it with an empty package."
|
||||
],
|
||||
"resolutions": {
|
||||
"@microsoft/api-extractor@^7.35.4": "patch:@microsoft/api-extractor@npm%3A7.35.4#./.yarn/patches/@microsoft-api-extractor-npm-7.35.4-5f4f0357b4.patch",
|
||||
"vectra@^0.4.4": "patch:vectra@npm%3A0.4.4#./.yarn/patches/vectra-npm-0.4.4-6aac3f6c29.patch",
|
||||
"domino@^2.1.6": "patch:domino@npm%3A2.1.6#./.yarn/patches/domino-npm-2.1.6-b0dc3de857.patch"
|
||||
"domino@^2.1.6": "patch:domino@npm%3A2.1.6#./.yarn/patches/domino-npm-2.1.6-b0dc3de857.patch",
|
||||
"canvas": "npm:empty-npm-package@1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sentry/cli": "^2.25.0",
|
||||
|
|
68
yarn.lock
68
yarn.lock
|
@ -4711,6 +4711,24 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@pdf-lib/standard-fonts@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "@pdf-lib/standard-fonts@npm:1.0.0"
|
||||
dependencies:
|
||||
pako: "npm:^1.0.6"
|
||||
checksum: 0dfcedbd16e3f48afcac321f153d81f49ea6030030fa1e05a07bde359629949e27b3d77b39a5c7b0b0ff8af09912765f62a911cd67d488d4dc0b3c1c5ca106f0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@pdf-lib/upng@npm:^1.0.1":
|
||||
version: 1.0.1
|
||||
resolution: "@pdf-lib/upng@npm:1.0.1"
|
||||
dependencies:
|
||||
pako: "npm:^1.0.10"
|
||||
checksum: eecac72f36d51b67acb09f61a7bbb55577ceb6232aa0c1a827aea003126787cede07ad391c0c789ab36164a9a86d1634768c2cf941148743e155de7ed6c34fc3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@peculiar/asn1-schema@npm:^2.3.6":
|
||||
version: 2.3.8
|
||||
resolution: "@peculiar/asn1-schema@npm:2.3.8"
|
||||
|
@ -10267,6 +10285,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"canvas@npm:empty-npm-package@1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "empty-npm-package@npm:1.0.0"
|
||||
checksum: 745b1e85c1c3f42d5960fc5729e6ad6114a41af9425a673b1f3493c0fb431273d48e30170bcfaf8141feca15f95ca141c8237aefee8ee84fd34586f06ee62368
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"capnp-ts@npm:^0.7.0":
|
||||
version: 0.7.0
|
||||
resolution: "capnp-ts@npm:0.7.0"
|
||||
|
@ -13275,6 +13300,8 @@ __metadata:
|
|||
dotenv: "npm:^16.3.1"
|
||||
lazyrepo: "npm:0.0.0-alpha.27"
|
||||
lodash: "npm:^4.17.21"
|
||||
pdf-lib: "npm:^1.17.1"
|
||||
pdfjs-dist: "npm:^4.0.379"
|
||||
react: "npm:^18.2.0"
|
||||
react-dom: "npm:^18.2.0"
|
||||
react-router-dom: "npm:^6.17.0"
|
||||
|
@ -19663,6 +19690,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pako@npm:^1.0.10, pako@npm:^1.0.11, pako@npm:^1.0.6":
|
||||
version: 1.0.11
|
||||
resolution: "pako@npm:1.0.11"
|
||||
checksum: 1ad07210e894472685564c4d39a08717e84c2a68a70d3c1d9e657d32394ef1670e22972a433cbfe48976cb98b154ba06855dcd3fcfba77f60f1777634bec48c0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pako@npm:~0.2.0":
|
||||
version: 0.2.9
|
||||
resolution: "pako@npm:0.2.9"
|
||||
|
@ -19907,6 +19941,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"path2d-polyfill@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "path2d-polyfill@npm:2.0.1"
|
||||
checksum: 82b26ef1a737560bc7a974919aec5d1c316d923b39cdf3aae2f20cd77b08d6c5256a5605f178d7dc010de7c886eeac0c263a590653687f8aea60fabbc0582890
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pathe@npm:^1.1.0, pathe@npm:^1.1.2":
|
||||
version: 1.1.2
|
||||
resolution: "pathe@npm:1.1.2"
|
||||
|
@ -19914,6 +19955,33 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pdf-lib@npm:^1.17.1":
|
||||
version: 1.17.1
|
||||
resolution: "pdf-lib@npm:1.17.1"
|
||||
dependencies:
|
||||
"@pdf-lib/standard-fonts": "npm:^1.0.0"
|
||||
"@pdf-lib/upng": "npm:^1.0.1"
|
||||
pako: "npm:^1.0.11"
|
||||
tslib: "npm:^1.11.1"
|
||||
checksum: ea8c2c0c813d89ca359a0c831cb2e8a581c569ed44b074316f917942b5baa101f878ce922f1cb87e6f41a035008ba75b52267480aee5d65a5e68c445ee83bc37
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pdfjs-dist@npm:^4.0.379":
|
||||
version: 4.0.379
|
||||
resolution: "pdfjs-dist@npm:4.0.379"
|
||||
dependencies:
|
||||
canvas: "npm:^2.11.2"
|
||||
path2d-polyfill: "npm:^2.0.1"
|
||||
dependenciesMeta:
|
||||
canvas:
|
||||
optional: true
|
||||
path2d-polyfill:
|
||||
optional: true
|
||||
checksum: 9abea0cd20e3a209537be81092055e247dc10cadc08c840ad4b2495e1e6d512b3f3208dfe12f47389af6b9f078a6e00b7bb9d950fe4dbe9f214ef726f8c2815f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"peek-stream@npm:^1.1.0":
|
||||
version: 1.1.3
|
||||
resolution: "peek-stream@npm:1.1.3"
|
||||
|
|
Loading…
Reference in a new issue