Custom Tools DX + screenshot example (#2198)

This PR adds a custom tool example, the `Screenshot Tool`.

It demonstrates how a user can create a custom tool together with custom
tool UI.

### Change Type

- [x] `minor` — New feature

### Test Plan

1. Use the screenshot example

### Release Notes

- adds ScreenshotTool custom tool example
- improvements and new exports related to copying and exporting images /
files
- loosens up types around icons and translations
- moving `StateNode.isActive` into an atom
- adding `Editor.path`
This commit is contained in:
Steve Ruiz 2023-11-15 18:06:02 +00:00 committed by GitHub
parent d683cc0943
commit 14e8d19a71
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
116 changed files with 2559 additions and 1519 deletions

View file

@ -1,249 +1,234 @@
export {}
import test, { Page, expect } from '@playwright/test'
import { Editor, TLShapeId, TLShapePartial } from '@tldraw/tldraw'
import assert from 'assert'
import { rename, writeFile } from 'fs/promises'
import { setupPage } from '../shared-e2e'
// import test, { Page, expect } from '@playwright/test'
// import { Editor, TLShapeId, TLShapePartial } from '@tldraw/tldraw'
// import assert from 'assert'
// import { rename, writeFile } from 'fs/promises'
// import { setupPage } from '../shared-e2e'
declare const editor: Editor
// declare const editor: Editor
test.describe('Export snapshots', () => {
const snapshots = {
'Exports geo text with leading line breaks': [
{
id: 'shape:testShape' as TLShapeId,
type: 'geo',
props: {
w: 100,
h: 30,
text: '\n\n\n\n\n\ntext',
},
},
],
'Exports geo text with trailing line breaks': [
{
id: 'shape:testShape' as TLShapeId,
type: 'geo',
props: {
w: 100,
h: 30,
text: 'text\n\n\n\n\n\n',
},
},
],
} as Record<string, TLShapePartial[]>
// test.describe('Export snapshots', () => {
// const snapshots = {
// 'Exports geo text with leading line breaks': [
// {
// id: 'shape:testShape' as TLShapeId,
// type: 'geo',
// props: {
// w: 100,
// h: 30,
// text: '\n\n\n\n\n\ntext',
// },
// },
// ],
// 'Exports geo text with trailing line breaks': [
// {
// id: 'shape:testShape' as TLShapeId,
// type: 'geo',
// props: {
// w: 100,
// h: 30,
// text: 'text\n\n\n\n\n\n',
// },
// },
// ],
// } as Record<string, TLShapePartial[]>
for (const fill of ['none', 'semi', 'solid', 'pattern']) {
snapshots[`geo fill=${fill}`] = [
{
id: 'shape:testShape' as TLShapeId,
type: 'geo',
props: {
fill,
color: 'green',
w: 100,
h: 100,
},
},
]
// for (const fill of ['none', 'semi', 'solid', 'pattern']) {
// snapshots[`geo fill=${fill}`] = [
// {
// id: 'shape:testShape' as TLShapeId,
// type: 'geo',
// props: {
// fill,
// color: 'green',
// w: 100,
// h: 100,
// },
// },
// ]
snapshots[`arrow fill=${fill}`] = [
{
id: 'shape:testShape' as TLShapeId,
type: 'arrow',
props: {
color: 'light-green',
fill: fill,
arrowheadStart: 'square',
arrowheadEnd: 'dot',
start: { type: 'point', x: 0, y: 0 },
end: { type: 'point', x: 100, y: 100 },
bend: 20,
},
},
]
// snapshots[`arrow fill=${fill}`] = [
// {
// id: 'shape:testShape' as TLShapeId,
// type: 'arrow',
// props: {
// color: 'light-green',
// fill: fill,
// arrowheadStart: 'square',
// arrowheadEnd: 'dot',
// start: { type: 'point', x: 0, y: 0 },
// end: { type: 'point', x: 100, y: 100 },
// bend: 20,
// },
// },
// ]
snapshots[`draw fill=${fill}`] = [
{
id: 'shape:testShape' as TLShapeId,
type: 'draw',
props: {
color: 'light-violet',
fill: fill,
segments: [
{
type: 'straight',
points: [{ x: 0, y: 0 }],
},
{
type: 'straight',
points: [
{ x: 0, y: 0 },
{ x: 100, y: 0 },
],
},
{
type: 'straight',
points: [
{ x: 100, y: 0 },
{ x: 0, y: 100 },
],
},
{
type: 'straight',
points: [
{ x: 0, y: 100 },
{ x: 100, y: 100 },
],
},
{
type: 'straight',
points: [
{ x: 100, y: 100 },
{ x: 0, y: 0 },
],
},
],
isClosed: true,
isComplete: true,
},
},
]
}
// snapshots[`draw fill=${fill}`] = [
// {
// id: 'shape:testShape' as TLShapeId,
// type: 'draw',
// props: {
// color: 'light-violet',
// fill: fill,
// segments: [
// {
// type: 'straight',
// points: [{ x: 0, y: 0 }],
// },
// {
// type: 'straight',
// points: [
// { x: 0, y: 0 },
// { x: 100, y: 0 },
// ],
// },
// {
// type: 'straight',
// points: [
// { x: 100, y: 0 },
// { x: 0, y: 100 },
// ],
// },
// {
// type: 'straight',
// points: [
// { x: 0, y: 100 },
// { x: 100, y: 100 },
// ],
// },
// {
// type: 'straight',
// points: [
// { x: 100, y: 100 },
// { x: 0, y: 0 },
// ],
// },
// ],
// isClosed: true,
// isComplete: true,
// },
// },
// ]
// }
for (const font of ['draw', 'sans', 'serif', 'mono']) {
snapshots[`geo font=${font}`] = [
{
id: 'shape:testShape' as TLShapeId,
type: 'geo',
props: {
text: 'test',
color: 'blue',
font,
w: 100,
h: 100,
},
},
]
// for (const font of ['draw', 'sans', 'serif', 'mono']) {
// snapshots[`geo font=${font}`] = [
// {
// id: 'shape:testShape' as TLShapeId,
// type: 'geo',
// props: {
// text: 'test',
// color: 'blue',
// font,
// w: 100,
// h: 100,
// },
// },
// ]
snapshots[`arrow font=${font}`] = [
{
id: 'shape:testShape' as TLShapeId,
type: 'arrow',
props: {
color: 'blue',
fill: 'solid',
arrowheadStart: 'square',
arrowheadEnd: 'arrow',
font,
start: { type: 'point', x: 0, y: 0 },
end: { type: 'point', x: 100, y: 100 },
bend: 20,
text: 'test',
},
},
]
// snapshots[`arrow font=${font}`] = [
// {
// id: 'shape:testShape' as TLShapeId,
// type: 'arrow',
// props: {
// color: 'blue',
// fill: 'solid',
// arrowheadStart: 'square',
// arrowheadEnd: 'arrow',
// font,
// start: { type: 'point', x: 0, y: 0 },
// end: { type: 'point', x: 100, y: 100 },
// bend: 20,
// text: 'test',
// },
// },
// ]
snapshots[`arrow font=${font}`] = [
{
id: 'shape:testShape' as TLShapeId,
type: 'arrow',
props: {
color: 'blue',
fill: 'solid',
arrowheadStart: 'square',
arrowheadEnd: 'arrow',
font,
start: { type: 'point', x: 0, y: 0 },
end: { type: 'point', x: 100, y: 100 },
bend: 20,
text: 'test',
},
},
]
// snapshots[`arrow font=${font}`] = [
// {
// id: 'shape:testShape' as TLShapeId,
// type: 'arrow',
// props: {
// color: 'blue',
// fill: 'solid',
// arrowheadStart: 'square',
// arrowheadEnd: 'arrow',
// font,
// start: { type: 'point', x: 0, y: 0 },
// end: { type: 'point', x: 100, y: 100 },
// bend: 20,
// text: 'test',
// },
// },
// ]
snapshots[`note font=${font}`] = [
{
id: 'shape:testShape' as TLShapeId,
type: 'note',
props: {
color: 'violet',
font,
text: 'test',
},
},
]
// snapshots[`note font=${font}`] = [
// {
// id: 'shape:testShape' as TLShapeId,
// type: 'note',
// props: {
// color: 'violet',
// font,
// text: 'test',
// },
// },
// ]
snapshots[`text font=${font}`] = [
{
id: 'shape:testShape' as TLShapeId,
type: 'text',
props: {
color: 'red',
font,
text: 'test',
},
},
]
}
// snapshots[`text font=${font}`] = [
// {
// id: 'shape:testShape' as TLShapeId,
// type: 'text',
// props: {
// color: 'red',
// font,
// text: 'test',
// },
// },
// ]
// }
const snapshotsToTest = Object.entries(snapshots)
const filteredSnapshots = snapshotsToTest // maybe we filter these down, there are a lot of them
// for (const [name, shapes] of Object.entries(snapshots)) {
// test(`Exports with ${name}`, async ({ browser }) => {
// const page = await browser.newPage()
// await setupPage(page)
// await page.evaluate((shapes) => {
// editor
// .updateInstanceState({ exportBackground: false })
// .selectAll()
// .deleteShapes(editor.selectedShapeIds)
// .createShapes(shapes)
// }, shapes as any)
for (const [name, shapes] of filteredSnapshots) {
test(`Exports with ${name} in dark mode`, async ({ browser }) => {
const page = await browser.newPage()
await setupPage(page)
await page.evaluate((shapes) => {
editor.user.updateUserPreferences({ isDarkMode: true })
editor
.updateInstanceState({ exportBackground: false })
.selectAll()
.deleteShapes(editor.selectedShapeIds)
.createShapes(shapes)
}, shapes as any)
// await snapshotTest(page)
// })
// }
await snapshotTest(page)
})
}
// for (const [name, shapes] of Object.entries(snapshots)) {
// test(`Exports with ${name} in dark mode`, async ({ browser }) => {
// const page = await browser.newPage()
// await setupPage(page)
// await page.evaluate((shapes) => {
// editor.user.updateUserPreferences({ isDarkMode: true })
// editor
// .updateInstanceState({ exportBackground: false })
// .selectAll()
// .deleteShapes(editor.selectedShapeIds)
// .createShapes(shapes)
// }, shapes as any)
async function snapshotTest(page: Page) {
page.waitForEvent('download').then(async (download) => {
const path = (await download.path()) as string
assert(path)
await rename(path, path + '.svg')
await writeFile(
path + '.html',
`
<!DOCTYPE html>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<img src="${path}.svg" />
`,
'utf-8'
)
// await snapshotTest(page)
// })
// }
// })
// async function snapshotTest(page: Page) {
// page.waitForEvent('download').then(async (download) => {
// const path = (await download.path()) as string
// assert(path)
// await rename(path, path + '.svg')
// await writeFile(
// path + '.html',
// `
// <!DOCTYPE html>
// <meta charset="utf-8" />
// <meta name="viewport" content="width=device-width, initial-scale=1" />
// <img src="${path}.svg" />
// `,
// 'utf-8'
// )
// await page.goto(`file://${path}.html`)
// const clip = await page.$eval('img', (img) => img.getBoundingClientRect())
// await expect(page).toHaveScreenshot({
// omitBackground: true,
// clip,
// })
// })
// await page.evaluate(() => (window as any)['tldraw-export']())
// }
await page.goto(`file://${path}.html`)
const clip = await page.$eval('img', (img) => img.getBoundingClientRect())
await expect(page).toHaveScreenshot({
omitBackground: true,
clip,
})
})
await page.evaluate(() => (window as any)['tldraw-export']())
}
})

View file

@ -0,0 +1,5 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 15C20 17.7614 17.7614 20 15 20C12.2386 20 10 17.7614 10 15C10 12.2386 12.2386 10 15 10C17.7614 10 20 12.2386 20 15Z" stroke="black" stroke-width="2"/>
<rect x="21" y="5" width="4" height="4" rx="2" fill="black"/>
<path d="M5 3H25C26.1046 3 27 3.89543 27 5V25C27 26.1046 26.1046 27 25 27H5C3.89543 27 3 26.1046 3 25V5C3 3.89543 3.89543 3 5 3Z" stroke="black" stroke-width="2"/>
</svg>

After

Width:  |  Height:  |  Size: 493 B

View file

@ -1,7 +1,7 @@
import { Tldraw, TLEditorComponents } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
const components: Partial<TLEditorComponents> = {
const components: TLEditorComponents = {
Brush: function MyBrush({ brush }) {
return (
<svg className="tl-overlays__item">

View file

@ -8,7 +8,7 @@ export const uiOverrides: TLUiOverrides = {
tools.card = {
id: 'card',
icon: 'color',
label: 'Card' as any,
label: 'Card',
kbd: 'c',
readonlyOk: false,
onSelect: () => {

View file

@ -72,7 +72,7 @@ const MyComponentInFront = track(() => {
)
})
const components: Partial<TLEditorComponents> = {
const components: TLEditorComponents = {
OnTheCanvas: MyComponent,
InFrontOfTheCanvas: MyComponentInFront,
SnapLine: null,

View file

@ -0,0 +1,50 @@
import { StateNode, TLCancelEvent, TLInterruptEvent } from '@tldraw/tldraw'
import { ScreenshotDragging } from './childStates/Dragging'
import { ScreenshotIdle } from './childStates/Idle'
import { ScreenshotPointing } from './childStates/Pointing'
// There's a guide at the bottom of this file!
export class ScreenshotTool extends StateNode {
// [1]
static override id = 'screenshot'
static override initial = 'idle'
static override children = () => [ScreenshotIdle, ScreenshotPointing, ScreenshotDragging]
// [2]
override onEnter = () => {
this.editor.setCursor({ type: 'cross', rotation: 0 })
}
override onExit = () => {
this.editor.setCursor({ type: 'default', rotation: 0 })
}
// [3]
override onInterrupt: TLInterruptEvent = () => {
this.complete()
}
override onCancel: TLCancelEvent = () => {
this.complete()
}
private complete() {
this.parent.transition('select', {})
}
}
/*
This file contains our screenshot tool. The tool is a StateNode with the `id` "screenshot".
[1]
It has three child state nodes, ScreenshotIdle, ScreenshotPointing, and ScreenshotDragging.
Its initial state is `idle`.
[2]
When the screenshot tool is entered, we set the cursor to a crosshair. When it is exited, we
set the cursor back to the default cursor.
[3]
When the screenshot tool is interrupted or cancelled, we transition back to the select tool.
*/

View file

@ -0,0 +1,123 @@
import { Box2d, StateNode, atom, copyAs, exportAs } from '@tldraw/tldraw'
// There's a guide at the bottom of this file!
export class ScreenshotDragging extends StateNode {
static override id = 'dragging'
// [1]
screenshotBox = atom('screenshot brush', new Box2d())
// [2]
override onEnter = () => {
this.update()
}
override onPointerMove = () => {
this.update()
}
override onKeyDown = () => {
this.update()
}
override onKeyUp = () => {
this.update()
}
private update() {
const {
inputs: { shiftKey, altKey, originPagePoint, currentPagePoint },
} = this.editor
const box = Box2d.FromPoints([originPagePoint, currentPagePoint])
if (shiftKey) {
if (box.w > box.h * (16 / 9)) {
box.h = box.w * (9 / 16)
} else {
box.w = box.h * (16 / 9)
}
if (currentPagePoint.x < originPagePoint.x) {
box.x = originPagePoint.x - box.w
}
if (currentPagePoint.y < originPagePoint.y) {
box.y = originPagePoint.y - box.h
}
}
if (altKey) {
box.w *= 2
box.h *= 2
box.x = originPagePoint.x - box.w / 2
box.y = originPagePoint.y - box.h / 2
}
this.screenshotBox.set(box)
}
// [3]
override onPointerUp = () => {
const { editor } = this
const box = this.screenshotBox.value
// get all shapes contained by or intersecting the box
const shapes = editor.currentPageShapes.filter((s) => {
const pageBounds = editor.getShapeMaskedPageBounds(s)
if (!pageBounds) return false
return box.includes(pageBounds)
})
if (shapes.length) {
if (editor.inputs.ctrlKey) {
// Copy the shapes to the clipboard
copyAs(
editor,
shapes.map((s) => s.id),
'png',
{ bounds: box, background: editor.instanceState.exportBackground }
)
} else {
// Export the shapes as a png
exportAs(
editor,
shapes.map((s) => s.id),
'png',
{ bounds: box, background: editor.instanceState.exportBackground }
)
}
}
this.editor.setCurrentTool('select')
}
// [4]
override onCancel = () => {
this.editor.setCurrentTool('select')
}
}
/*
[1]
This state has a reactive property (an Atom) called "screenshotBox". This is the box
that the user is drawing on the screen as they drag their pointer. We use an Atom here
so that our UI can subscribe to this property using `useValue` (see the ScreenshotBox
component in ScreenshotToolExample).
[2]
When the user enters this state, or when they move their pointer, we update the
screenshotBox property to be drawn between the place where the user started pointing
and the place where their pointer is now. If the user is holding Shift, then we modify
the dimensions of this box so that it is in a 16:9 aspect ratio.
[3]
When the user makes a pointer up and stops dragging, we export the shapes contained by
the screenshot box as a png. If the user is holding the ctrl key, we copy the shapes
to the clipboard instead.
[4]
When the user cancels (esc key) or makes a pointer up event, we transition back to the
select tool.
*/

View file

@ -0,0 +1,17 @@
import { StateNode, TLEventHandlers } from '@tldraw/tldraw'
// There's a guide at the bottom of this file!
export class ScreenshotIdle extends StateNode {
static override id = 'idle'
// [1]
override onPointerDown: TLEventHandlers['onPointerUp'] = () => {
this.parent.transition('pointing')
}
}
/*
[1]
When we the user makes a pointer down event, we transition to the pointing state.
*/

View file

@ -0,0 +1,38 @@
import { StateNode, TLEventHandlers } from '@tldraw/tldraw'
// There's a guide at the bottom of this file!
export class ScreenshotPointing extends StateNode {
static override id = 'pointing'
// [1]
override onPointerMove: TLEventHandlers['onPointerUp'] = () => {
if (this.editor.inputs.isDragging) {
this.parent.transition('dragging')
}
}
// [2]
override onPointerUp: TLEventHandlers['onPointerUp'] = () => {
this.complete()
}
override onCancel: TLEventHandlers['onCancel'] = () => {
this.complete()
}
private complete() {
this.parent.transition('idle')
}
}
/*
[1]
When the user makes a pointer move event, we check if they are dragging. If they are,
we transition to the dragging state. If they are not yet dragging, we stay in this state.
[2]
When the user cancelles or makes a pointer up event (while this state is still active,
so after the user has started pointing but before they've moved their pointer far enough
to start dragging), then we transition back to the idle state.
*/

View file

@ -0,0 +1,157 @@
import {
Box2d,
TLEditorComponents,
TLUiAssetUrlOverrides,
TLUiOverrides,
Tldraw,
toolbarItem,
useEditor,
useValue,
} from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
import { ScreenshotTool } from './ScreenshotTool/ScreenshotTool'
import { ScreenshotDragging } from './ScreenshotTool/childStates/Dragging'
// There's a guide at the bottom of this file!
// [1]
const customTools = [ScreenshotTool]
// [2]
const customUiOverrides: TLUiOverrides = {
tools: (editor, tools) => {
return {
...tools,
screenshot: {
id: 'screenshot',
label: 'Screenshot',
readonlyOk: false,
icon: 'tool-screenshot',
kbd: 'j',
onSelect() {
editor.setCurrentTool('screenshot')
},
},
}
},
toolbar: (_editor, toolbarItems, { tools }) => {
toolbarItems.splice(4, 0, toolbarItem(tools.screenshot))
return toolbarItems
},
}
// [3]
const customAssetUrls: TLUiAssetUrlOverrides = {
icons: {
'tool-screenshot': '/tool-screenshot.svg',
},
}
// [4]
function ScreenshotBox() {
const editor = useEditor()
const screenshotBrush = useValue(
'screenshot brush',
() => {
// Check whether the screenshot tool (and its dragging state) is active
if (editor.getPath() !== 'screenshot.dragging') return null
// Get screenshot.dragging state node
const draggingState = editor.getStateDescendant<ScreenshotDragging>('screenshot.dragging')!
// Get the box from the screenshot.dragging state node
const box = draggingState.screenshotBox.get()
// The box is in "page space", i.e. panned and zoomed with the canvas, but we
// want to show it in front of the canvas, so we'll need to convert it to
// "page space", i.e. uneffected by scale, and relative to the tldraw
// page's top left corner.
const { zoomLevel } = editor
const { x, y } = editor.pageToScreen({ x: box.x, y: box.y })
return new Box2d(x, y, box.w * zoomLevel, box.h * zoomLevel)
},
[editor]
)
if (!screenshotBrush) return null
return (
<div
style={{
position: 'absolute',
top: 0,
left: 0,
transform: `translate(${screenshotBrush.x}px, ${screenshotBrush.y}px)`,
width: screenshotBrush.w,
height: screenshotBrush.h,
border: '1px solid var(--color-text-0)',
zIndex: 999,
}}
/>
)
}
const customComponents: TLEditorComponents = {
InFrontOfTheCanvas: () => {
return <ScreenshotBox />
},
}
// [5]
export default function ScreenshotToolExample() {
return (
<div className="tldraw__editor">
<Tldraw
persistenceKey="tldraw_screenshot_example"
tools={customTools}
overrides={customUiOverrides}
assetUrls={customAssetUrls}
components={customComponents}
/>
</div>
)
}
/*
Introduction:
This example shows how to create a custom tool. In tldraw, tools are parts of the
tldraw state chart. While the most common use for tools is creating shapes, you can
use tools to create other types of interactions too! In this example, we create a
"screenshot tool" that lets the user draw a box on the canvas. When the user finishes
drawing their box, we'll export (or copy) a screenshot of that area.
[1]
Our custom tool is a class that extends the StateNode class. See the ScreenshotTool
files for more about the too. We define an array (outside of any React component)
to hold the custom tools. We'll pass this into the Tldraw component's `tools` prop.
[2]
Here we add our custom tool to the toolbar. We do this by providing a custom
toolbar override to the Tldraw component. This override is a function that takes
the current editor, the default toolbar items, and the default tools. It returns
the new toolbar items. We use the toolbarItem helper to create a new toolbar item
for our custom tool. We then splice it into the toolbar items array at the 4th index.
This puts it after the eraser tool. We'll pass our overrides object into the
Tldraw component's `overrides` prop.
[3]
Our toolbar item is using a custom icon, so we need to provide the asset url for it.
We do this by providing a custom assetUrls object to the Tldraw component.
This object is a map of icon ids to their urls. The icon ids are the same as the
icon prop on the toolbar item. We'll pass our assetUrls object into the Tldraw
component's `assetUrls` prop.
[4]
We want to show a box on the canvas when the screenshot tool is active. We do this
by providing an override to the InFrontOfTheCanvas component. This component will be shown
in front of the canvas but behind any other UI elements, such as menus and the toolbar.
We'll pass our components object into the Tldraw component's `components` prop.
[5]
Finally we pass all of our customizations into the Tldraw component. It's important
that the customizations are defined outside of the React component, otherwise they
will cause the Tldraw component to see them as new values on every render, which may
produce unexpected results.
*/

View file

@ -30,6 +30,7 @@ import MultipleExample from './examples/MultipleExample'
import OnTheCanvasExample from './examples/OnTheCanvas'
import PersistenceExample from './examples/PersistenceExample'
import ReadOnlyExample from './examples/ReadOnlyExample'
import ScreenshotToolExample from './examples/ScreenshotToolExample/ScreenshotToolExample'
import ScrollExample from './examples/ScrollExample'
import ShapeMetaExample from './examples/ShapeMetaExample'
import SnapshotExample from './examples/SnapshotExample/SnapshotExample'
@ -116,6 +117,11 @@ export const allExamples: Example[] = [
path: 'custom-ui',
element: <CustomUiExample />,
},
{
title: 'Custom Tool (Screenshot)',
path: 'screenshot-tool',
element: <ScreenshotToolExample />,
},
{
title: 'Hide UI',
path: 'hide-ui',

View file

@ -58,6 +58,7 @@ const menuOverrides = {
schema.forEach((item) => {
if (item.id === 'menu' && item.type === 'group') {
item.children = item.children.filter((menuItem) => {
if (!menuItem) return false
if (menuItem.id === 'file' && menuItem.type === 'submenu') {
return false
}

View file

@ -275,6 +275,9 @@ export class Box2d {
zeroFix(): this;
}
// @public (undocumented)
export type BoxLike = Box2d | Box2dModel;
// @internal (undocumented)
export const CAMERA_SLIDE_FRICTION = 0.09;
@ -625,7 +628,7 @@ export class Editor extends EventEmitter<TLEventMap> {
// @deprecated (undocumented)
get currentPageState(): TLInstancePageState;
// @deprecated (undocumented)
get currentTool(): StateNode | undefined;
get currentTool(): StateNode;
// @deprecated (undocumented)
get currentToolId(): string;
deleteAssets(assets: TLAsset[] | TLAssetId[]): this;
@ -701,7 +704,7 @@ export class Editor extends EventEmitter<TLEventMap> {
getCurrentPageShapes(): TLShape[];
getCurrentPageShapesSorted(): TLShape[];
getCurrentPageState(): TLInstancePageState;
getCurrentTool(): StateNode | undefined;
getCurrentTool(): StateNode;
getCurrentToolId(): string;
getDocumentSettings(): TLDocument;
getDroppingOverShape(point: VecLike, droppingShapes?: TLShape[]): TLUnknownShape | undefined;
@ -783,16 +786,10 @@ export class Editor extends EventEmitter<TLEventMap> {
getSharedOpacity(): SharedStyle<number>;
getSharedStyles(): ReadonlySharedStyleMap;
getSortedChildIdsForParent(parent: TLPage | TLParentId | TLShape): TLShapeId[];
getStateDescendant(path: string): StateNode | undefined;
getStateDescendant<T extends StateNode>(path: string): T | undefined;
// @internal (undocumented)
getStyleForNextShape<T>(style: StyleProp<T>): T;
getSvg(shapes: TLShape[] | TLShapeId[], opts?: Partial<{
scale: number;
background: boolean;
padding: number;
darkMode?: boolean | undefined;
preserveAspectRatio: React.SVGAttributes<SVGSVGElement>['preserveAspectRatio'];
}>): Promise<SVGSVGElement | undefined>;
getSvg(shapes: TLShape[] | TLShapeId[], opts?: Partial<TLSvgOptions>): Promise<SVGSVGElement | undefined>;
getViewportPageBounds(): Box2d;
getViewportPageCenter(): Vec2d;
getViewportScreenBounds(): Box2d;
@ -2128,7 +2125,7 @@ export interface TldrawEditorBaseProps {
autoFocus?: boolean;
children?: any;
className?: string;
components?: Partial<TLEditorComponents>;
components?: TLEditorComponents;
inferDarkMode?: boolean;
initialState?: string;
onMount?: TLOnMountHandler;
@ -2150,13 +2147,9 @@ export type TldrawEditorProps = TldrawEditorBaseProps & ({
});
// @public (undocumented)
export type TLEditorComponents = {
export type TLEditorComponents = Partial<{
[K in keyof BaseEditorComponents]: BaseEditorComponents[K] | null;
} & {
ErrorFallback: TLErrorFallbackComponent;
ShapeErrorFallback: TLShapeErrorFallbackComponent;
ShapeIndicatorErrorFallback: TLShapeIndicatorErrorFallbackComponent;
};
} & ErrorComponents>;
// @public (undocumented)
export interface TLEditorOptions {
@ -2682,6 +2675,16 @@ export type TLStoreWithStatus = {
// @public (undocumented)
export type TLSvgDefsComponent = React.ComponentType;
// @public (undocumented)
export type TLSvgOptions = {
bounds: Box2d;
scale: number;
background: boolean;
padding: number;
darkMode?: boolean;
preserveAspectRatio: React.SVGAttributes<SVGSVGElement>['preserveAspectRatio'];
};
// @public (undocumented)
export type TLTickEvent = (elapsed: number) => void;

View file

@ -3770,6 +3770,42 @@
],
"implementsTokenRanges": []
},
{
"kind": "TypeAlias",
"canonicalReference": "@tldraw/editor!BoxLike:type",
"docComment": "/**\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export type BoxLike = "
},
{
"kind": "Reference",
"text": "Box2d",
"canonicalReference": "@tldraw/editor!Box2d:class"
},
{
"kind": "Content",
"text": " | "
},
{
"kind": "Reference",
"text": "Box2dModel",
"canonicalReference": "@tldraw/tlschema!Box2dModel:interface"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/editor/src/lib/primitives/Box2d.ts",
"releaseTag": "Public",
"name": "BoxLike",
"typeTokenRange": {
"startIndex": 1,
"endIndex": 4
}
},
{
"kind": "Function",
"canonicalReference": "@tldraw/editor!canonicalizeRotation:function(1)",
@ -8256,10 +8292,6 @@
"text": "StateNode",
"canonicalReference": "@tldraw/editor!StateNode:class"
},
{
"kind": "Content",
"text": " | undefined"
},
{
"kind": "Content",
"text": ";"
@ -8271,7 +8303,7 @@
"name": "currentTool",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 3
"endIndex": 2
},
"isStatic": false,
"isProtected": false,
@ -10447,10 +10479,6 @@
"text": "StateNode",
"canonicalReference": "@tldraw/editor!StateNode:class"
},
{
"kind": "Content",
"text": " | undefined"
},
{
"kind": "Content",
"text": ";"
@ -10459,7 +10487,7 @@
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 1,
"endIndex": 3
"endIndex": 2
},
"releaseTag": "Public",
"isProtected": false,
@ -13658,7 +13686,16 @@
"excerptTokens": [
{
"kind": "Content",
"text": "getStateDescendant(path: "
"text": "getStateDescendant<T extends "
},
{
"kind": "Reference",
"text": "StateNode",
"canonicalReference": "@tldraw/editor!StateNode:class"
},
{
"kind": "Content",
"text": ">(path: "
},
{
"kind": "Content",
@ -13668,24 +13705,32 @@
"kind": "Content",
"text": "): "
},
{
"kind": "Reference",
"text": "StateNode",
"canonicalReference": "@tldraw/editor!StateNode:class"
},
{
"kind": "Content",
"text": " | undefined"
"text": "T | undefined"
},
{
"kind": "Content",
"text": ";"
}
],
"typeParameters": [
{
"typeParameterName": "T",
"constraintTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"defaultTypeTokenRange": {
"startIndex": 0,
"endIndex": 0
}
}
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 3,
"endIndex": 5
"startIndex": 5,
"endIndex": 6
},
"releaseTag": "Public",
"isProtected": false,
@ -13694,8 +13739,8 @@
{
"parameterName": "path",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
"startIndex": 3,
"endIndex": 4
},
"isOptional": false
}
@ -13740,27 +13785,18 @@
"text": "Partial",
"canonicalReference": "!Partial:type"
},
{
"kind": "Content",
"text": "<{\n scale: number;\n background: boolean;\n padding: number;\n darkMode?: boolean | undefined;\n preserveAspectRatio: "
},
{
"kind": "Reference",
"text": "React.SVGAttributes",
"canonicalReference": "@types/react!React.SVGAttributes:interface"
},
{
"kind": "Content",
"text": "<"
},
{
"kind": "Reference",
"text": "SVGSVGElement",
"canonicalReference": "!SVGSVGElement:interface"
"text": "TLSvgOptions",
"canonicalReference": "@tldraw/editor!TLSvgOptions:type"
},
{
"kind": "Content",
"text": ">['preserveAspectRatio'];\n }>"
"text": ">"
},
{
"kind": "Content",
@ -13791,8 +13827,8 @@
],
"isStatic": false,
"returnTypeTokenRange": {
"startIndex": 13,
"endIndex": 17
"startIndex": 11,
"endIndex": 15
},
"releaseTag": "Public",
"isProtected": false,
@ -13810,7 +13846,7 @@
"parameterName": "opts",
"parameterTypeTokenRange": {
"startIndex": 6,
"endIndex": 12
"endIndex": 10
},
"isOptional": true
}
@ -37739,24 +37775,11 @@
"kind": "Content",
"text": "components?: "
},
{
"kind": "Reference",
"text": "Partial",
"canonicalReference": "!Partial:type"
},
{
"kind": "Content",
"text": "<"
},
{
"kind": "Reference",
"text": "TLEditorComponents",
"canonicalReference": "@tldraw/editor!TLEditorComponents:type"
},
{
"kind": "Content",
"text": ">"
},
{
"kind": "Content",
"text": ";"
@ -37768,7 +37791,7 @@
"name": "components",
"propertyTypeTokenRange": {
"startIndex": 1,
"endIndex": 5
"endIndex": 2
}
},
{
@ -38050,9 +38073,14 @@
"kind": "Content",
"text": "export type TLEditorComponents = "
},
{
"kind": "Reference",
"text": "Partial",
"canonicalReference": "!Partial:type"
},
{
"kind": "Content",
"text": "{\n [K in keyof "
"text": "<{\n [K in keyof "
},
{
"kind": "Reference",
@ -38070,34 +38098,16 @@
},
{
"kind": "Content",
"text": "[K] | null;\n} & {\n ErrorFallback: "
"text": "[K] | null;\n} & "
},
{
"kind": "Reference",
"text": "TLErrorFallbackComponent",
"canonicalReference": "@tldraw/editor!~TLErrorFallbackComponent:type"
"text": "ErrorComponents",
"canonicalReference": "@tldraw/editor!~ErrorComponents:type"
},
{
"kind": "Content",
"text": ";\n ShapeErrorFallback: "
},
{
"kind": "Reference",
"text": "TLShapeErrorFallbackComponent",
"canonicalReference": "@tldraw/editor!~TLShapeErrorFallbackComponent:type"
},
{
"kind": "Content",
"text": ";\n ShapeIndicatorErrorFallback: "
},
{
"kind": "Reference",
"text": "TLShapeIndicatorErrorFallbackComponent",
"canonicalReference": "@tldraw/editor!~TLShapeIndicatorErrorFallbackComponent:type"
},
{
"kind": "Content",
"text": ";\n}"
"text": ">"
},
{
"kind": "Content",
@ -38109,7 +38119,7 @@
"name": "TLEditorComponents",
"typeTokenRange": {
"startIndex": 1,
"endIndex": 12
"endIndex": 9
}
},
{
@ -42979,6 +42989,59 @@
"endIndex": 2
}
},
{
"kind": "TypeAlias",
"canonicalReference": "@tldraw/editor!TLSvgOptions:type",
"docComment": "/**\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export type TLSvgOptions = "
},
{
"kind": "Content",
"text": "{\n bounds: "
},
{
"kind": "Reference",
"text": "Box2d",
"canonicalReference": "@tldraw/editor!Box2d:class"
},
{
"kind": "Content",
"text": ";\n scale: number;\n background: boolean;\n padding: number;\n darkMode?: boolean;\n preserveAspectRatio: "
},
{
"kind": "Reference",
"text": "React.SVGAttributes",
"canonicalReference": "@types/react!React.SVGAttributes:interface"
},
{
"kind": "Content",
"text": "<"
},
{
"kind": "Reference",
"text": "SVGSVGElement",
"canonicalReference": "!SVGSVGElement:interface"
},
{
"kind": "Content",
"text": ">['preserveAspectRatio'];\n}"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/editor/src/lib/editor/types/misc-types.ts",
"releaseTag": "Public",
"name": "TLSvgOptions",
"typeTokenRange": {
"startIndex": 1,
"endIndex": 8
}
},
{
"kind": "TypeAlias",
"canonicalReference": "@tldraw/editor!TLTickEvent:type",

View file

@ -326,6 +326,11 @@ input,
fill: var(--color-brush-fill);
}
.tl-screenshot-brush {
stroke: var(--color-text-0);
fill: none;
}
/* -------------------- Scribble -------------------- */
.tl-scribble {
@ -1558,7 +1563,6 @@ it from receiving any pointer events or affecting the cursor. */
overflow: auto;
font-size: 12px;
max-height: 320px;
}
.tl-error-boundary__content button {

View file

@ -241,7 +241,7 @@ export {
type TLHistoryEntry,
type TLHistoryMark,
} from './lib/editor/types/history-types'
export { type RequiredKeys } from './lib/editor/types/misc-types'
export { type RequiredKeys, type TLSvgOptions } from './lib/editor/types/misc-types'
export { type TLResizeHandle, type TLSelectionHandle } from './lib/editor/types/selection-types'
export { useContainer } from './lib/hooks/useContainer'
export { getCursor } from './lib/hooks/useCursor'
@ -260,6 +260,7 @@ export {
Box2d,
ROTATE_CORNER_TO_SELECTION_CORNER,
rotateSelectionHandle,
type BoxLike,
type RotateCorner,
type SelectionCorner,
type SelectionEdge,

View file

@ -86,7 +86,7 @@ export interface TldrawEditorBaseProps {
/**
* Overrides for the editor's components, such as handles, collaborator cursors, etc.
*/
components?: Partial<TLEditorComponents>
components?: TLEditorComponents
/**
* Called when the editor has mounted.

View file

@ -184,7 +184,7 @@ function ZoomBrushWrapper() {
if (!(ZoomBrush && zoomBrush)) return null
return <ZoomBrush className="tl-user-brush" brush={zoomBrush} />
return <ZoomBrush className="tl-user-brush tl-zoom-brush" brush={zoomBrush} />
}
function SnapLinesWrapper() {

View file

@ -12,7 +12,7 @@ export type TLBrushComponent = ComponentType<{
}>
/** @public */
export const DefaultBrush: TLBrushComponent = ({ brush, color, opacity }) => {
export const DefaultBrush: TLBrushComponent = ({ brush, color, opacity, className }) => {
const rSvg = useRef<SVGSVGElement>(null)
useTransform(rSvg, brush.x, brush.y)
@ -27,7 +27,7 @@ export const DefaultBrush: TLBrushComponent = ({ brush, color, opacity }) => {
<rect width={w} height={h} fill="none" stroke={color} opacity={0.1} />
</g>
) : (
<rect className="tl-brush tl-brush__default" width={w} height={h} />
<rect className={`tl-brush tl-brush__default ${className}`} width={w} height={h} />
)}
</svg>
)

View file

@ -129,7 +129,7 @@ import {
} from './types/event-types'
import { TLExternalAssetContent, TLExternalContent } from './types/external-content'
import { TLCommandHistoryOptions } from './types/history-types'
import { OptionalKeys, RequiredKeys } from './types/misc-types'
import { OptionalKeys, RequiredKeys, TLSvgOptions } from './types/misc-types'
import { TLResizeHandle } from './types/selection-types'
/** @public */
@ -1127,13 +1127,14 @@ export class Editor extends EventEmitter<TLEventMap> {
this.root.transition(id, info)
return this
}
/**
* The current selected tool.
*
* @public
*/
@computed getCurrentTool(): StateNode | undefined {
return this.root.getCurrent()
@computed getCurrentTool(): StateNode {
return this.root.getCurrent()!
}
/**
@ -1175,17 +1176,17 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @public
*/
getStateDescendant(path: string): StateNode | undefined {
getStateDescendant<T extends StateNode>(path: string): T | undefined {
const ids = path.split('.').reverse()
let state = this.root as StateNode
while (ids.length > 0) {
const id = ids.pop()
if (!id) return state
if (!id) return state as T
const childState = state.children?.[id]
if (!childState) return undefined
state = childState
}
return state
return state as T
}
/* ---------------- Document Settings --------------- */
@ -7550,6 +7551,9 @@ export class Editor extends EventEmitter<TLEventMap> {
// Otherwise, just return an empty map.
const currentTool = this.root.getCurrent()!
const styles = new SharedStyleMap()
if (!currentTool) return styles
if (currentTool.shapeType) {
for (const style of this.styleProps[currentTool.shapeType].keys()) {
styles.applyValue(style, this.getStyleForNextShape(style))
@ -8370,16 +8374,7 @@ export class Editor extends EventEmitter<TLEventMap> {
*
* @public
*/
async getSvg(
shapes: TLShapeId[] | TLShape[],
opts = {} as Partial<{
scale: number
background: boolean
padding: number
darkMode?: boolean
preserveAspectRatio: React.SVGAttributes<SVGSVGElement>['preserveAspectRatio']
}>
) {
async getSvg(shapes: TLShapeId[] | TLShape[], opts = {} as Partial<TLSvgOptions>) {
const ids =
typeof shapes[0] === 'string'
? (shapes as TLShapeId[])
@ -8406,12 +8401,16 @@ export class Editor extends EventEmitter<TLEventMap> {
// --- Common bounding box of all shapes
let bbox = null
for (const { maskedPageBounds } of renderingShapes) {
if (!maskedPageBounds) continue
if (bbox) {
bbox.union(maskedPageBounds)
} else {
bbox = maskedPageBounds.clone()
if (opts.bounds) {
bbox = opts.bounds
} else {
for (const { maskedPageBounds } of renderingShapes) {
if (!maskedPageBounds) continue
if (bbox) {
bbox.union(maskedPageBounds)
} else {
bbox = maskedPageBounds.clone()
}
}
}

View file

@ -12,10 +12,8 @@ export class RootState extends StateNode {
case 'KeyZ': {
if (!(info.shiftKey || info.ctrlKey)) {
const currentTool = this.getCurrent()
if (currentTool && currentTool.getCurrent()?.id === 'idle') {
if (this.children!['zoom']) {
this.editor.setCurrentTool('zoom', { ...info, onInteractionEnd: currentTool.id })
}
if (currentTool && currentTool.getCurrent()?.id === 'idle' && this.children!['zoom']) {
this.editor.setCurrentTool('zoom', { ...info, onInteractionEnd: currentTool.id })
}
}
break

View file

@ -1,4 +1,16 @@
import { Box2d } from '../../primitives/Box2d'
/** @public */
export type RequiredKeys<T, K extends keyof T> = Partial<Omit<T, K>> & Pick<T, K>
/** @public */
export type OptionalKeys<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
/** @public */
export type TLSvgOptions = {
bounds: Box2d
scale: number
background: boolean
padding: number
darkMode?: boolean
preserveAspectRatio: React.SVGAttributes<SVGSVGElement>['preserveAspectRatio']
}

View file

@ -55,7 +55,7 @@ export function useDocumentEvents() {
if (
e.altKey &&
// todo: When should we allow the alt key to be used? Perhaps states should declare which keys matter to them?
(editor.isIn('zoom') || !editor.root.getPath().endsWith('.idle')) &&
(editor.isIn('zoom') || !editor.getPath().endsWith('.idle')) &&
!isFocusingInput()
) {
// On windows the alt key opens the menu bar.

View file

@ -74,19 +74,24 @@ export interface BaseEditorComponents {
InFrontOfTheCanvas: TLInFrontOfTheCanvas
}
/** @public */
export type TLEditorComponents = {
[K in keyof BaseEditorComponents]: BaseEditorComponents[K] | null
} & {
// These will always have defaults
type ErrorComponents = {
ErrorFallback: TLErrorFallbackComponent
ShapeErrorFallback: TLShapeErrorFallbackComponent
ShapeIndicatorErrorFallback: TLShapeIndicatorErrorFallbackComponent
}
const EditorComponentsContext = createContext({} as TLEditorComponents)
/** @public */
export type TLEditorComponents = Partial<
{
[K in keyof BaseEditorComponents]: BaseEditorComponents[K] | null
} & ErrorComponents
>
const EditorComponentsContext = createContext({} as TLEditorComponents & ErrorComponents)
type ComponentsContextProviderProps = {
overrides?: Partial<TLEditorComponents>
overrides?: TLEditorComponents
children: any
}
@ -99,6 +104,7 @@ export function EditorComponentsProvider({ overrides, children }: ComponentsCont
SvgDefs: DefaultSvgDefs,
Brush: DefaultBrush,
ZoomBrush: DefaultBrush,
ScreenshotBrush: DefaultBrush,
CollaboratorBrush: DefaultBrush,
Cursor: DefaultCursor,
CollaboratorCursor: DefaultCursor,

View file

@ -2,6 +2,9 @@ import { Box2dModel } from '@tldraw/tlschema'
import { Vec2d, VecLike } from './Vec2d'
import { PI, PI2, toPrecision } from './utils'
/** @public */
export type BoxLike = Box2dModel | Box2d
/** @public */
export type SelectionEdge = 'top' | 'right' | 'bottom' | 'left'

View file

@ -99,6 +99,7 @@ import { TLShapeUtilCanvasSvgDef } from '@tldraw/editor';
import { TLShapeUtilFlag } from '@tldraw/editor';
import { TLStore } from '@tldraw/editor';
import { TLStoreWithStatus } from '@tldraw/editor';
import { TLSvgOptions } from '@tldraw/editor';
import { TLTextShape } from '@tldraw/editor';
import { TLUnknownShape } from '@tldraw/editor';
import { TLVideoShape } from '@tldraw/editor';
@ -286,6 +287,9 @@ export const ContextMenu: ({ children }: {
children: any;
}) => JSX.Element;
// @public
export function copyAs(editor: Editor, ids: TLShapeId[], format?: TLCopyType, opts?: Partial<TLSvgOptions>): Promise<void>;
// @public (undocumented)
export const DEFAULT_ACCEPTED_IMG_TYPE: string[];
@ -461,8 +465,11 @@ export type EventsProviderProps = {
children: any;
};
// @public
export function exportAs(editor: Editor, ids: TLShapeId[], format?: TLExportType, opts?: Partial<TLSvgOptions>): Promise<void>;
// @public (undocumented)
export function findMenuItem(menu: TLUiMenuSchema, path: string[]): TLUiMenuChild;
export function findMenuItem(menu: TLUiMenuSchema, path: string[]): TLUiCustomMenuItem | TLUiMenuGroup | TLUiMenuItem | TLUiSubMenu<string>;
// @public (undocumented)
function Footer({ className, children }: {
@ -906,7 +913,7 @@ export function menuCustom(id: string, opts?: Partial<{
};
// @public (undocumented)
export function menuGroup(id: string, ...children: (false | null | TLUiMenuChild)[]): null | TLUiMenuGroup;
export function menuGroup(id: string, ...children: (false | TLUiMenuChild)[]): null | TLUiMenuGroup;
// @public (undocumented)
export function menuItem(actionItem: TLUiActionItem | TLUiToolItem, opts?: Partial<{
@ -915,7 +922,7 @@ export function menuItem(actionItem: TLUiActionItem | TLUiToolItem, opts?: Parti
}>): TLUiMenuItem;
// @public (undocumented)
export function menuSubmenu(id: string, label: TLUiTranslationKey, ...children: (false | null | TLUiMenuChild)[]): null | TLUiSubMenu;
export function menuSubmenu(id: string, label: Exclude<string, TLUiTranslationKey> | TLUiTranslationKey, ...children: (false | TLUiMenuChild)[]): null | TLUiSubMenu;
// @public (undocumented)
export class NoteShapeTool extends StateNode {
@ -1020,7 +1027,7 @@ export class NoteShapeUtil extends ShapeUtil<TLNoteShape> {
export function OfflineIndicator(): JSX.Element;
// @internal (undocumented)
export function parseAndLoadDocument(editor: Editor, document: string, msg: (id: TLUiTranslationKey) => string, addToast: TLUiToastsContextType['addToast'], onV1FileLoad?: () => void, forceDarkMode?: boolean): Promise<void>;
export function parseAndLoadDocument(editor: Editor, document: string, msg: (id: Exclude<string, TLUiTranslationKey> | TLUiTranslationKey) => string, addToast: TLUiToastsContextType['addToast'], onV1FileLoad?: () => void, forceDarkMode?: boolean): Promise<void>;
// @public (undocumented)
export function parseTldrawJsonFile({ json, schema, }: {
@ -1081,7 +1088,7 @@ function SubContent({ alignOffset, sideOffset, children, }: {
// @public (undocumented)
function SubTrigger({ label, 'data-testid': testId, 'data-direction': dataDirection, }: {
label: TLUiTranslationKey;
label: Exclude<string, TLUiTranslationKey> | TLUiTranslationKey;
'data-testid'?: string;
'data-direction'?: 'left' | 'right';
}): JSX.Element;
@ -1211,17 +1218,7 @@ function Title({ className, children }: {
}): JSX.Element;
// @public (undocumented)
export function Tldraw(props: TldrawEditorBaseProps & ({
store: TLStore | TLStoreWithStatus;
} | {
store?: undefined;
persistenceKey?: string;
sessionId?: string;
defaultName?: string;
snapshot?: StoreSnapshot<TLRecord>;
}) & TldrawUiProps & Partial<TLExternalContentProps> & {
assetUrls?: RecursivePartial<TLEditorAssetUrls>;
}): JSX.Element;
export function Tldraw(props: TldrawProps): JSX.Element;
// @public (undocumented)
export const TLDRAW_FILE_EXTENSION: ".tldr";
@ -1257,6 +1254,17 @@ export const TldrawHandles: TLHandlesComponent;
// @public (undocumented)
export const TldrawHoveredShapeIndicator: TLHoveredShapeIndicatorComponent;
// @public (undocumented)
export type TldrawProps = TldrawEditorBaseProps & ({
store: TLStore | TLStoreWithStatus;
} | {
store?: undefined;
persistenceKey?: string;
sessionId?: string;
defaultName?: string;
snapshot?: StoreSnapshot<TLRecord>;
}) & TldrawUiProps & Partial<TLExternalContentProps>;
// @public (undocumented)
export const TldrawScribble: TLScribbleComponent;
@ -1271,6 +1279,7 @@ export const TldrawUi: React_2.NamedExoticComponent<TldrawUiProps>;
// @public
export interface TldrawUiBaseProps {
assetUrls?: TLUiAssetUrlOverrides;
children?: ReactNode;
hideUi?: boolean;
renderDebugMenuItems?: () => React_2.ReactNode;
@ -1295,27 +1304,27 @@ export interface TldrawUiContextProviderProps {
export type TldrawUiProps = TldrawUiBaseProps & TldrawUiContextProviderProps;
// @public (undocumented)
export interface TLUiActionItem {
export interface TLUiActionItem<TransationKey extends string = string, IconType extends string = string> {
// (undocumented)
checkbox?: boolean;
// (undocumented)
contextMenuLabel?: TLUiTranslationKey;
contextMenuLabel?: TransationKey;
// (undocumented)
icon?: TLUiIconType;
icon?: IconType;
// (undocumented)
id: string;
// (undocumented)
kbd?: string;
// (undocumented)
label?: TLUiTranslationKey;
label?: TransationKey;
// (undocumented)
menuLabel?: TLUiTranslationKey;
menuLabel?: TransationKey;
// (undocumented)
onSelect: (source: TLUiEventSource) => Promise<void> | void;
// (undocumented)
readonlyOk: boolean;
// (undocumented)
shortcutsLabel?: TLUiTranslationKey;
shortcutsLabel?: TransationKey;
// (undocumented)
title?: string;
}
@ -1326,14 +1335,17 @@ export type TLUiActionsContextType = Record<string, TLUiActionItem>;
// @public (undocumented)
export type TLUiActionsMenuSchemaContextType = TLUiMenuSchema;
// @public (undocumented)
export type TLUiAssetUrlOverrides = RecursivePartial<TLUiAssetUrls>;
// @public (undocumented)
export interface TLUiButtonProps extends React_3.HTMLAttributes<HTMLButtonElement> {
// (undocumented)
disabled?: boolean;
// (undocumented)
icon?: TLUiIconType;
icon?: Exclude<string, TLUiIconType> | TLUiIconType;
// (undocumented)
iconLeft?: TLUiIconType;
iconLeft?: Exclude<string, TLUiIconType> | TLUiIconType;
// (undocumented)
invertIcon?: boolean;
// (undocumented)
@ -1341,7 +1353,7 @@ export interface TLUiButtonProps extends React_3.HTMLAttributes<HTMLButtonElemen
// (undocumented)
kbd?: string;
// (undocumented)
label?: TLUiTranslationKey;
label?: Exclude<string, TLUiTranslationKey> | TLUiTranslationKey;
// (undocumented)
loading?: boolean;
// (undocumented)
@ -1419,7 +1431,7 @@ export interface TLUiIconProps extends React.HTMLProps<HTMLDivElement> {
// (undocumented)
crossOrigin?: 'anonymous' | 'use-credentials';
// (undocumented)
icon: TLUiIconType;
icon: Exclude<string, TLUiIconType> | TLUiIconType;
// (undocumented)
invertIcon?: boolean;
// (undocumented)
@ -1444,11 +1456,11 @@ export interface TLUiInputProps {
// (undocumented)
disabled?: boolean;
// (undocumented)
icon?: TLUiIconType;
icon?: Exclude<string, TLUiIconType> | TLUiIconType;
// (undocumented)
iconLeft?: TLUiIconType;
iconLeft?: Exclude<string, TLUiIconType> | TLUiIconType;
// (undocumented)
label?: TLUiTranslationKey;
label?: Exclude<string, TLUiTranslationKey> | TLUiTranslationKey;
// (undocumented)
onBlur?: (value: string) => void;
// (undocumented)
@ -1477,7 +1489,7 @@ export type TLUiKeyboardShortcutsSchemaProviderProps = {
};
// @public (undocumented)
export type TLUiMenuChild = TLUiCustomMenuItem | TLUiMenuGroup | TLUiMenuItem | TLUiSubMenu;
export type TLUiMenuChild<TranslationKey extends string = string> = null | TLUiCustomMenuItem | TLUiMenuGroup | TLUiMenuItem | TLUiSubMenu<TranslationKey>;
// @public (undocumented)
export type TLUiMenuGroup = {
@ -1518,32 +1530,23 @@ export type TLUiMenuSchemaProviderProps = {
};
// @public (undocumented)
export interface TLUiOverrides {
// (undocumented)
actions?: WithDefaultHelpers<NonNullable<ActionsProviderProps['overrides']>>;
// (undocumented)
actionsMenu?: WithDefaultHelpers<NonNullable<ActionsMenuSchemaProviderProps['overrides']>>;
// (undocumented)
contextMenu?: WithDefaultHelpers<NonNullable<TLUiContextMenuSchemaProviderProps['overrides']>>;
// (undocumented)
helpMenu?: WithDefaultHelpers<NonNullable<TLUiHelpMenuSchemaProviderProps['overrides']>>;
// (undocumented)
keyboardShortcutsMenu?: WithDefaultHelpers<NonNullable<TLUiKeyboardShortcutsSchemaProviderProps['overrides']>>;
// (undocumented)
menu?: WithDefaultHelpers<NonNullable<TLUiMenuSchemaProviderProps['overrides']>>;
// (undocumented)
toolbar?: WithDefaultHelpers<NonNullable<TLUiToolbarSchemaProviderProps['overrides']>>;
// (undocumented)
tools?: WithDefaultHelpers<NonNullable<TLUiToolsProviderProps['overrides']>>;
// (undocumented)
translations?: TLUiTranslationProviderProps['overrides'];
}
export type TLUiOverrides = Partial<{
actionsMenu: WithDefaultHelpers<NonNullable<ActionsMenuSchemaProviderProps['overrides']>>;
actions: WithDefaultHelpers<NonNullable<ActionsProviderProps['overrides']>>;
contextMenu: WithDefaultHelpers<NonNullable<TLUiContextMenuSchemaProviderProps['overrides']>>;
helpMenu: WithDefaultHelpers<NonNullable<TLUiHelpMenuSchemaProviderProps['overrides']>>;
menu: WithDefaultHelpers<NonNullable<TLUiMenuSchemaProviderProps['overrides']>>;
toolbar: WithDefaultHelpers<NonNullable<TLUiToolbarSchemaProviderProps['overrides']>>;
keyboardShortcutsMenu: WithDefaultHelpers<NonNullable<TLUiKeyboardShortcutsSchemaProviderProps['overrides']>>;
tools: WithDefaultHelpers<NonNullable<TLUiToolsProviderProps['overrides']>>;
translations: TLUiTranslationProviderProps['overrides'];
}>;
// @public (undocumented)
export type TLUiSubMenu = {
export type TLUiSubMenu<TranslationKey extends string = string> = {
id: string;
type: 'submenu';
label: TLUiTranslationKey;
label: TranslationKey;
disabled: boolean;
readonlyOk: boolean;
children: TLUiMenuChild[];
@ -1599,15 +1602,15 @@ export type TLUiToolbarItem = {
export type TLUiToolbarSchemaContextType = TLUiToolbarItem[];
// @public (undocumented)
export interface TLUiToolItem {
export interface TLUiToolItem<TranslationKey extends string = string, IconType extends string = string> {
// (undocumented)
icon: TLUiIconType;
icon: IconType;
// (undocumented)
id: string;
// (undocumented)
kbd?: string;
// (undocumented)
label: TLUiTranslationKey;
label: TranslationKey;
// (undocumented)
meta?: {
[key: string]: any;
@ -1617,7 +1620,7 @@ export interface TLUiToolItem {
// (undocumented)
readonlyOk: boolean;
// (undocumented)
shortcutsLabel?: TLUiTranslationKey;
shortcutsLabel?: TranslationKey;
}
// @public (undocumented)
@ -1681,7 +1684,7 @@ export function useCanUndo(): boolean;
export function useContextMenuSchema(): TLUiMenuSchema;
// @public (undocumented)
export function useCopyAs(): (ids?: TLShapeId[], format?: TLCopyType) => void;
export function useCopyAs(): (ids: TLShapeId[], format?: TLCopyType) => void;
// @public (undocumented)
export function useDefaultHelpers(): {
@ -1696,7 +1699,7 @@ export function useDefaultHelpers(): {
clearDialogs: () => void;
removeDialog: (id: string) => string;
updateDialog: (id: string, newDialogData: Partial<TLUiDialog>) => string;
msg: (id: TLUiTranslationKey) => string;
msg: (id: string) => string;
isMobile: boolean;
};
@ -1704,7 +1707,7 @@ export function useDefaultHelpers(): {
export function useDialogs(): TLUiDialogsContextType;
// @public (undocumented)
export function useExportAs(): (ids?: TLShapeId[], format?: TLExportType) => Promise<void>;
export function useExportAs(): (ids: TLShapeId[], format?: TLExportType) => void;
// @public (undocumented)
export function useHelpMenuSchema(): TLUiMenuSchema;
@ -1747,7 +1750,7 @@ export function useToolbarSchema(): TLUiToolbarSchemaContextType;
export function useTools(): TLUiToolsContextType;
// @public
export function useTranslation(): (id: TLUiTranslationKey) => string;
export function useTranslation(): (id: Exclude<string, TLUiTranslationKey> | string) => string;
// @public (undocumented)
export function useUiEvents(): TLUiEventContextType;

File diff suppressed because it is too large Load diff

View file

@ -2,7 +2,7 @@
// eslint-disable-next-line local/no-export-star
export * from '@tldraw/editor'
export { Tldraw } from './lib/Tldraw'
export { Tldraw, type TldrawProps } from './lib/Tldraw'
export { TldrawCropHandles, type TldrawCropHandlesProps } from './lib/canvas/TldrawCropHandles'
export { TldrawHandles } from './lib/canvas/TldrawHandles'
export { TldrawHoveredShapeIndicator } from './lib/canvas/TldrawHoveredShapeIndicator'
@ -43,7 +43,7 @@ export {
TldrawUiContextProvider,
type TldrawUiContextProviderProps,
} from './lib/ui/TldrawUiContextProvider'
export { setDefaultUiAssetUrls } from './lib/ui/assetUrls'
export { setDefaultUiAssetUrls, type TLUiAssetUrlOverrides } from './lib/ui/assetUrls'
export { ContextMenu, type TLUiContextMenuProps } from './lib/ui/components/ContextMenu'
export { OfflineIndicator } from './lib/ui/components/OfflineIndicator/OfflineIndicator'
export { Spinner } from './lib/ui/components/Spinner'
@ -142,16 +142,22 @@ export {
} from './lib/ui/hooks/useTranslation/useTranslation'
export { type TLUiIconType } from './lib/ui/icon-types'
export { useDefaultHelpers, type TLUiOverrides } from './lib/ui/overrides'
export { setDefaultEditorAssetUrls } from './lib/utils/assetUrls'
export {
DEFAULT_ACCEPTED_IMG_TYPE,
DEFAULT_ACCEPTED_VID_TYPE,
containBoxSize,
getResizedImageDataUrl,
isGifAnimated,
} from './lib/utils/assets'
export { buildFromV1Document, type LegacyTldrawDocument } from './lib/utils/buildFromV1Document'
export { getEmbedInfo } from './lib/utils/embeds'
} from './lib/utils/assets/assets'
export { getEmbedInfo } from './lib/utils/embeds/embeds'
export { copyAs } from './lib/utils/export/copyAs'
export { exportAs } from './lib/utils/export/exportAs'
export { setDefaultEditorAssetUrls } from './lib/utils/static-assets/assetUrls'
export { truncateStringWithEllipsis } from './lib/utils/text/text'
export {
buildFromV1Document,
type LegacyTldrawDocument,
} from './lib/utils/tldr/buildFromV1Document'
export {
TLDRAW_FILE_EXTENSION,
parseAndLoadDocument,
@ -159,8 +165,7 @@ export {
serializeTldrawJson,
serializeTldrawJsonBlob,
type TldrawFile,
} from './lib/utils/file'
export { truncateStringWithEllipsis } from './lib/utils/text'
} from './lib/utils/tldr/file'
export { Dialog, DropdownMenu }
import * as Dialog from './lib/ui/components/primitives/Dialog'
import * as DropdownMenu from './lib/ui/components/primitives/DropdownMenu'

View file

@ -3,7 +3,6 @@ import {
Editor,
ErrorScreen,
LoadingScreen,
RecursivePartial,
StoreSnapshot,
TLOnMountHandler,
TLRecord,
@ -31,35 +30,31 @@ import { registerDefaultSideEffects } from './defaultSideEffects'
import { defaultTools } from './defaultTools'
import { TldrawUi, TldrawUiProps } from './ui/TldrawUi'
import { ContextMenu } from './ui/components/ContextMenu'
import { TLEditorAssetUrls, useDefaultEditorAssetsWithOverrides } from './utils/assetUrls'
import { usePreloadAssets } from './utils/usePreloadAssets'
import { usePreloadAssets } from './ui/hooks/usePreloadAssets'
import { useDefaultEditorAssetsWithOverrides } from './utils/static-assets/assetUrls'
/** @public */
export function Tldraw(
props: TldrawEditorBaseProps &
(
| {
store: TLStore | TLStoreWithStatus
}
| {
store?: undefined
persistenceKey?: string
sessionId?: string
defaultName?: string
/**
* A snapshot to load for the store's initial data / schema.
*/
snapshot?: StoreSnapshot<TLRecord>
}
) &
TldrawUiProps &
Partial<TLExternalContentProps> & {
/**
* Urls for the editor to find fonts and other assets.
*/
assetUrls?: RecursivePartial<TLEditorAssetUrls>
}
) {
export type TldrawProps = TldrawEditorBaseProps &
(
| {
store: TLStore | TLStoreWithStatus
}
| {
store?: undefined
persistenceKey?: string
sessionId?: string
defaultName?: string
/**
* A snapshot to load for the store's initial data / schema.
*/
snapshot?: StoreSnapshot<TLRecord>
}
) &
TldrawUiProps &
Partial<TLExternalContentProps>
/** @public */
export function Tldraw(props: TldrawProps) {
const {
children,
maxImageDimension,

View file

@ -17,9 +17,9 @@ import {
getHashForString,
} from '@tldraw/editor'
import { FONT_FAMILIES, FONT_SIZES, TEXT_PROPS } from './shapes/shared/default-shape-constants'
import { containBoxSize, getResizedImageDataUrl, isGifAnimated } from './utils/assets'
import { getEmbedInfo } from './utils/embeds'
import { cleanupText, isRightToLeftLanguage, truncateStringWithEllipsis } from './utils/text'
import { containBoxSize, getResizedImageDataUrl, isGifAnimated } from './utils/assets/assets'
import { getEmbedInfo } from './utils/embeds/embeds'
import { cleanupText, isRightToLeftLanguage, truncateStringWithEllipsis } from './utils/text/text'
/** @public */
export type TLExternalContentProps = {

View file

@ -26,7 +26,7 @@ export class Idle extends StateNode {
) {
this.editor.setCurrentTool('select')
this.editor.setEditingShape(onlySelectedShape.id)
this.editor.root.getCurrent()!.transition('editing_shape', {
this.editor.root.getCurrent()?.transition('editing_shape', {
...info,
target: 'shape',
shape: onlySelectedShape,

View file

@ -16,9 +16,9 @@ import {
stopEventPropagation,
toDomPrecision,
} from '@tldraw/editor'
import { getRotatedBoxShadow } from '../../utils/rotated-box-shadow'
import { truncateStringWithEllipsis } from '../../utils/text'
import { truncateStringWithEllipsis } from '../../utils/text/text'
import { HyperlinkButton } from '../shared/HyperlinkButton'
import { getRotatedBoxShadow } from '../shared/rotated-box-shadow'
/** @public */
export class BookmarkShapeUtil extends BaseBoxShapeUtil<TLBookmarkShape> {

View file

@ -15,9 +15,9 @@ import {
useValue,
} from '@tldraw/editor'
import { useMemo } from 'react'
import { getEmbedInfo, getEmbedInfoUnsafely } from '../../utils/embeds'
import { getRotatedBoxShadow } from '../../utils/rotated-box-shadow'
import { getEmbedInfo, getEmbedInfoUnsafely } from '../../utils/embeds/embeds'
import { resizeBox } from '../shared/resizeBox'
import { getRotatedBoxShadow } from '../shared/rotated-box-shadow'
const getSandboxPermissions = (permissions: TLEmbedShapePermissions) => {
return Object.entries(permissions)

View file

@ -23,7 +23,7 @@ export class Idle extends StateNode {
) {
this.editor.setCurrentTool('select')
this.editor.setEditingShape(onlySelectedShape.id)
this.editor.root.getCurrent()!.transition('editing_shape', {
this.editor.root.getCurrent()?.transition('editing_shape', {
...info,
target: 'shape',
shape: onlySelectedShape,

View file

@ -1,6 +1,6 @@
import { Vec2d } from '@tldraw/editor'
export const ROTATING_BOX_SHADOWS = [
const ROTATING_BOX_SHADOWS = [
{
offsetX: 0,
offsetY: 2,

View file

@ -32,7 +32,7 @@ export class Idle extends StateNode {
) {
this.editor.setCurrentTool('select')
this.editor.setEditingShape(onlySelectedShape.id)
this.editor.root.getCurrent()!.transition('editing_shape', {
this.editor.root.getCurrent()?.transition('editing_shape', {
...info,
target: 'shape',
shape: onlySelectedShape,

View file

@ -1,7 +1,7 @@
import { StateNode } from '@tldraw/editor'
import { Erasing } from './children/Erasing'
import { Idle } from './children/Idle'
import { Pointing } from './children/Pointing'
import { Erasing } from './childStates/Erasing'
import { Idle } from './childStates/Idle'
import { Pointing } from './childStates/Pointing'
/** @public */
export class EraserTool extends StateNode {

View file

@ -1,7 +1,7 @@
import { EASINGS, StateNode, TLClickEvent } from '@tldraw/editor'
import { Dragging } from './children/Dragging'
import { Idle } from './children/Idle'
import { Pointing } from './children/Pointing'
import { Dragging } from './childStates/Dragging'
import { Idle } from './childStates/Idle'
import { Pointing } from './childStates/Pointing'
/** @public */
export class HandTool extends StateNode {

View file

@ -1,6 +1,6 @@
import { StateNode } from '@tldraw/editor'
import { Idle } from './children/Idle'
import { Lasering } from './children/Lasering'
import { Idle } from './childStates/Idle'
import { Lasering } from './childStates/Lasering'
/** @public */
export class LaserTool extends StateNode {

View file

@ -1,21 +1,21 @@
import { StateNode } from '@tldraw/editor'
import { Brushing } from './children/Brushing'
import { Crop } from './children/Crop/Crop'
import { Cropping } from './children/Cropping'
import { DraggingHandle } from './children/DraggingHandle'
import { EditingShape } from './children/EditingShape'
import { Idle } from './children/Idle'
import { PointingCanvas } from './children/PointingCanvas'
import { PointingCropHandle } from './children/PointingCropHandle'
import { PointingHandle } from './children/PointingHandle'
import { PointingResizeHandle } from './children/PointingResizeHandle'
import { PointingRotateHandle } from './children/PointingRotateHandle'
import { PointingSelection } from './children/PointingSelection'
import { PointingShape } from './children/PointingShape'
import { Resizing } from './children/Resizing'
import { Rotating } from './children/Rotating'
import { ScribbleBrushing } from './children/ScribbleBrushing'
import { Translating } from './children/Translating'
import { Brushing } from './childStates/Brushing'
import { Crop } from './childStates/Crop/Crop'
import { Cropping } from './childStates/Cropping'
import { DraggingHandle } from './childStates/DraggingHandle'
import { EditingShape } from './childStates/EditingShape'
import { Idle } from './childStates/Idle'
import { PointingCanvas } from './childStates/PointingCanvas'
import { PointingCropHandle } from './childStates/PointingCropHandle'
import { PointingHandle } from './childStates/PointingHandle'
import { PointingResizeHandle } from './childStates/PointingResizeHandle'
import { PointingRotateHandle } from './childStates/PointingRotateHandle'
import { PointingSelection } from './childStates/PointingSelection'
import { PointingShape } from './childStates/PointingShape'
import { Resizing } from './childStates/Resizing'
import { Rotating } from './childStates/Rotating'
import { ScribbleBrushing } from './childStates/ScribbleBrushing'
import { Translating } from './childStates/Translating'
/** @public */
export class SelectTool extends StateNode {

View file

@ -1,7 +1,7 @@
import { StateNode, TLInterruptEvent, TLKeyboardEvent, TLPointerEventInfo } from '@tldraw/editor'
import { Idle } from './children/Idle'
import { Pointing } from './children/Pointing'
import { ZoomBrushing } from './children/ZoomBrushing'
import { Idle } from './childStates/Idle'
import { Pointing } from './childStates/Pointing'
import { ZoomBrushing } from './childStates/ZoomBrushing'
/** @public */
export class ZoomTool extends StateNode {

View file

@ -3,6 +3,7 @@ import { useEditor, useValue } from '@tldraw/editor'
import classNames from 'classnames'
import React, { ReactNode } from 'react'
import { TldrawUiContextProvider, TldrawUiContextProviderProps } from './TldrawUiContextProvider'
import { TLUiAssetUrlOverrides } from './assetUrls'
import { BackToContent } from './components/BackToContent'
import { DebugPanel } from './components/DebugPanel'
import { Dialogs } from './components/Dialogs'
@ -61,6 +62,9 @@ export interface TldrawUiBaseProps {
* Additional items to add to the debug menu (will be deprecated)
*/
renderDebugMenuItems?: () => React.ReactNode
/** Asset URL override. */
assetUrls?: TLUiAssetUrlOverrides
}
/**

View file

@ -1,14 +1,17 @@
import { EMBED_DEFINITIONS, LANGUAGES, RecursivePartial } from '@tldraw/editor'
import { version } from '../ui/version'
import { TLEditorAssetUrls, defaultEditorAssetUrls } from '../utils/assetUrls'
import { TLEditorAssetUrls, defaultEditorAssetUrls } from '../utils/static-assets/assetUrls'
import { TLUiIconType, iconTypes } from './icon-types'
export type TLUiAssetUrls = TLEditorAssetUrls & {
icons: Record<TLUiIconType, string>
icons: Record<TLUiIconType | Exclude<string, TLUiIconType>, string>
translations: Record<(typeof LANGUAGES)[number]['locale'], string>
embedIcons: Record<(typeof EMBED_DEFINITIONS)[number]['type'], string>
}
/** @public */
export type TLUiAssetUrlOverrides = RecursivePartial<TLUiAssetUrls>
export let defaultUiAssetUrls: TLUiAssetUrls = {
...defaultEditorAssetUrls,
icons: Object.fromEntries(

View file

@ -16,6 +16,7 @@ export const ActionsMenu = memo(function ActionsMenu() {
const isReadonly = useReadonly()
function getActionMenuItem(item: TLUiMenuChild) {
if (!item) return null
if (isReadonly && !item.readonlyOk) return null
switch (item.type) {

View file

@ -7,7 +7,9 @@ import { useBreakpoint } from '../hooks/useBreakpoint'
import { useContextMenuSchema } from '../hooks/useContextMenuSchema'
import { useMenuIsOpen } from '../hooks/useMenuIsOpen'
import { useReadonly } from '../hooks/useReadonly'
import { TLUiTranslationKey } from '../hooks/useTranslation/TLUiTranslationKey'
import { useTranslation } from '../hooks/useTranslation/useTranslation'
import { TLUiIconType } from '../icon-types'
import { MoveToPageMenu } from './MoveToPageMenu'
import { Button } from './primitives/Button'
import { Icon } from './primitives/Icon'
@ -116,6 +118,7 @@ const ContextMenuContent = forwardRef(function ContextMenuContent() {
parent: TLUiMenuChild | null,
depth: number
) {
if (!item) return null
if (isReadonly && !item.readonlyOk) return null
switch (item.type) {
@ -147,7 +150,7 @@ const ContextMenuContent = forwardRef(function ContextMenuContent() {
<_ContextMenu.SubTrigger dir="ltr" disabled={item.disabled} asChild>
<Button
type="menu"
label={item.label}
label={item.label as TLUiTranslationKey}
data-testid={`menu-item.${item.id}`}
icon="chevron-right"
/>
@ -165,7 +168,7 @@ const ContextMenuContent = forwardRef(function ContextMenuContent() {
const { id, checkbox, contextMenuLabel, label, onSelect, kbd, icon } = item.actionItem
const labelToUse = contextMenuLabel ?? label
const labelStr = labelToUse ? msg(labelToUse) : undefined
const labelStr = labelToUse ? msg(labelToUse as TLUiTranslationKey) : undefined
if (checkbox) {
// Item is in a checkbox group
@ -199,9 +202,9 @@ const ContextMenuContent = forwardRef(function ContextMenuContent() {
type="menu"
data-testid={`menu-item.${id}`}
kbd={kbd}
label={labelToUse}
label={labelToUse as TLUiTranslationKey}
disabled={item.disabled}
iconLeft={breakpoint < 3 && depth > 2 ? icon : undefined}
iconLeft={breakpoint < 3 && depth > 2 ? (icon as TLUiIconType) : undefined}
onClick={() => {
if (disableClicks) {
setDisableClicks(false)

View file

@ -65,7 +65,7 @@ export const DebugPanel = React.memo(function DebugPanel({
const CurrentState = track(function CurrentState() {
const editor = useEditor()
return <div className="tlui-debug-panel__current-state">{editor.root.getPath()}</div>
return <div className="tlui-debug-panel__current-state">{editor.getPath()}</div>
})
const ShapeCount = function ShapeCount() {

View file

@ -1,6 +1,6 @@
import { EMBED_DEFINITIONS, EmbedDefinition, track, useEditor } from '@tldraw/editor'
import { useRef, useState } from 'react'
import { TLEmbedResult, getEmbedInfo } from '../../utils/embeds'
import { TLEmbedResult, getEmbedInfo } from '../../utils/embeds/embeds'
import { useAssetUrls } from '../hooks/useAssetUrls'
import { TLUiDialogProps } from '../hooks/useDialogsProvider'
import { untranslated, useTranslation } from '../hooks/useTranslation/useTranslation'

View file

@ -13,8 +13,8 @@ import { Button } from './primitives/Button'
import * as M from './primitives/DropdownMenu'
interface HelpMenuLink {
label: TLUiTranslationKey
icon: TLUiIconType
label: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>
icon: TLUiIconType | Exclude<string, TLUiIconType>
url: string
}
@ -64,6 +64,7 @@ function HelpMenuContent() {
const isReadonly = useReadonly()
function getHelpMenuItem(item: TLUiMenuChild) {
if (!item) return null
if (isReadonly && !item.readonlyOk) return null
switch (item.type) {

View file

@ -12,6 +12,7 @@ export const KeyboardShortcutsDialog = () => {
const shortcutsItems = useKeyboardShortcutsSchema()
function getKeyboardShortcutItem(item: TLUiMenuChild) {
if (!item) return null
if (isReadonly && !item.readonlyOk) return null
switch (item.type) {
@ -23,7 +24,7 @@ export const KeyboardShortcutsDialog = () => {
</h2>
<div className="tlui-shortcuts-dialog__group__content">
{item.children
.filter((item) => item.type === 'item' && item.actionItem.kbd)
.filter((item) => item && item.type === 'item' && item.actionItem.kbd)
.map(getKeyboardShortcutItem)}
</div>
</div>

View file

@ -45,6 +45,7 @@ function MenuContent() {
parent: TLUiMenuChild | null,
depth: number
) {
if (!item) return null
switch (item.type) {
case 'custom': {
if (isReadonly && !item.readonlyOk) return null

View file

@ -11,9 +11,9 @@ import { StyleValuesForUi } from './styles'
interface DoubleDropdownPickerProps<T extends string> {
uiTypeA: string
uiTypeB: string
label: TLUiTranslationKey
labelA: TLUiTranslationKey
labelB: TLUiTranslationKey
label: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>
labelA: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>
labelB: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>
itemsA: StyleValuesForUi<T>
itemsB: StyleValuesForUi<T>
styleA: StyleProp<T>

View file

@ -10,7 +10,7 @@ import { StyleValuesForUi } from './styles'
interface DropdownPickerProps<T extends string> {
id: string
label?: TLUiTranslationKey
label?: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>
uiType: string
style: StyleProp<T>
value: SharedStyle<T>

View file

@ -11,10 +11,10 @@ import { Kbd } from './Kbd'
export interface TLUiButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
loading?: boolean // TODO: loading spinner
disabled?: boolean
label?: TLUiTranslationKey
icon?: TLUiIconType
label?: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>
icon?: TLUiIconType | Exclude<string, TLUiIconType>
spinner?: boolean
iconLeft?: TLUiIconType
iconLeft?: TLUiIconType | Exclude<string, TLUiIconType>
smallIcon?: boolean
kbd?: string
isChecked?: boolean

View file

@ -96,7 +96,7 @@ export function SubTrigger({
'data-testid': testId,
'data-direction': dataDirection,
}: {
label: TLUiTranslationKey
label: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>
'data-testid'?: string
'data-direction'?: 'left' | 'right'
}) {

View file

@ -5,7 +5,7 @@ import { TLUiIconType } from '../../icon-types'
/** @public */
export interface TLUiIconProps extends React.HTMLProps<HTMLDivElement> {
icon: TLUiIconType
icon: TLUiIconType | Exclude<string, TLUiIconType>
small?: boolean
color?: string
children?: undefined
@ -23,17 +23,21 @@ export const Icon = memo(function Icon({
...props
}: TLUiIconProps) {
const assetUrls = useAssetUrls()
const asset = assetUrls.icons[icon]
const asset = assetUrls.icons[icon as TLUiIconType] ?? assetUrls.icons['question-mark-circle']
const ref = useRef<HTMLDivElement>(null)
useLayoutEffect(() => {
if (!asset) {
console.error(`Icon not found: ${icon}. Add it to the assetUrls.icons object.`)
}
if (ref?.current) {
// HACK: Fix for <https://linear.app/tldraw/issue/TLD-1700/dragging-around-with-the-handtool-makes-lots-of-requests-for-icons>
// It seems that passing `WebkitMask` to react will cause a render on each call, no idea why... but this appears to be the fix.
// @ts-ignore
ref.current.style.webkitMask = `url(${asset}) center 100% / 100% no-repeat`
}
}, [ref, asset])
}, [ref, asset, icon])
return (
<div

View file

@ -9,9 +9,9 @@ import { Icon } from './Icon'
/** @public */
export interface TLUiInputProps {
disabled?: boolean
label?: TLUiTranslationKey
icon?: TLUiIconType
iconLeft?: TLUiIconType
label?: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>
icon?: TLUiIconType | Exclude<string, TLUiIconType>
iconLeft?: TLUiIconType | Exclude<string, TLUiIconType>
autofocus?: boolean
autoselect?: boolean
children?: any

View file

@ -13,7 +13,12 @@ import { TLUiToolItem } from './useTools'
import { TLUiTranslationKey } from './useTranslation/TLUiTranslationKey'
/** @public */
export type TLUiMenuChild = TLUiMenuItem | TLUiSubMenu | TLUiMenuGroup | TLUiCustomMenuItem
export type TLUiMenuChild<TranslationKey extends string = string> =
| TLUiMenuItem
| TLUiSubMenu<TranslationKey>
| TLUiMenuGroup
| TLUiCustomMenuItem
| null
/** @public */
export type TLUiCustomMenuItem = {
@ -44,10 +49,10 @@ export type TLUiMenuGroup = {
}
/** @public */
export type TLUiSubMenu = {
export type TLUiSubMenu<TranslationKey extends string = string> = {
id: string
type: 'submenu'
label: TLUiTranslationKey
label: TranslationKey
disabled: boolean
readonlyOk: boolean
children: TLUiMenuChild[]
@ -64,7 +69,7 @@ export function compactMenuItems<T>(arr: T[]): Exclude<T, null | false | undefin
/** @public */
export function menuGroup(
id: string,
...children: (TLUiMenuChild | null | false)[]
...children: (TLUiMenuChild | false)[]
): TLUiMenuGroup | null {
const childItems = compactMenuItems(children)
@ -83,8 +88,8 @@ export function menuGroup(
/** @public */
export function menuSubmenu(
id: string,
label: TLUiTranslationKey,
...children: (TLUiMenuChild | null | false)[]
label: TLUiTranslationKey | Exclude<string, TLUiTranslationKey>,
...children: (TLUiMenuChild | false)[]
): TLUiSubMenu | null {
const childItems = compactMenuItems(children)
if (childItems.length === 0) return null
@ -216,14 +221,11 @@ export function findMenuItem(menu: TLUiMenuSchema, path: string[]) {
return item
}
function _findMenuItem(
menu: TLUiMenuSchema | TLUiMenuChild[],
path: string[]
): TLUiMenuChild | null {
function _findMenuItem(menu: TLUiMenuSchema | TLUiMenuChild[], path: string[]): TLUiMenuChild {
const [next, ...rest] = path
if (!next) return null
const item = menu.find((item) => item.id === next)
const item = menu.find((item) => item?.id === next)
if (!item) return null
switch (item.type) {

View file

@ -17,7 +17,7 @@ import {
useEditor,
} from '@tldraw/editor'
import * as React from 'react'
import { getEmbedInfo } from '../../utils/embeds'
import { getEmbedInfo } from '../../utils/embeds/embeds'
import { EditLinkDialog } from '../components/EditLinkDialog'
import { EmbedDialog } from '../components/EmbedDialog'
import { TLUiIconType } from '../icon-types'
@ -32,15 +32,18 @@ import { useToasts } from './useToastsProvider'
import { TLUiTranslationKey } from './useTranslation/TLUiTranslationKey'
/** @public */
export interface TLUiActionItem {
icon?: TLUiIconType
export interface TLUiActionItem<
TransationKey extends string = string,
IconType extends string = string
> {
icon?: IconType
id: string
kbd?: string
title?: string
label?: TLUiTranslationKey
menuLabel?: TLUiTranslationKey
shortcutsLabel?: TLUiTranslationKey
contextMenuLabel?: TLUiTranslationKey
label?: TransationKey
menuLabel?: TransationKey
shortcutsLabel?: TransationKey
contextMenuLabel?: TransationKey
readonlyOk: boolean
checkbox?: boolean
onSelect: (source: TLUiEventSource) => Promise<void> | void
@ -98,7 +101,7 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
return editor.getSelectedShapeIds().length > 0
}
const actions = makeActions([
const actionItems: TLUiActionItem<TLUiTranslationKey, TLUiIconType>[] = [
{
id: 'edit-link',
label: 'action.edit-link',
@ -1094,7 +1097,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
editor.toggleLock(editor.getSelectedShapeIds())
},
},
])
]
const actions = makeActions(actionItems)
if (overrides) {
return overrides(editor, actions, undefined)
@ -1102,9 +1107,9 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) {
return actions
}, [
editor,
trackEvent,
overrides,
editor,
addDialog,
insertMedia,
exportAs,

View file

@ -1,6 +1,6 @@
import { Editor, TLShapeId, useEditor } from '@tldraw/editor'
import { TLShapeId, useEditor } from '@tldraw/editor'
import { useCallback } from 'react'
import { TLCopyType, getSvgAsImage, getSvgAsString } from '../../utils/export'
import { TLCopyType, copyAs } from '../../utils/export/copyAs'
import { useToasts } from './useToastsProvider'
import { useTranslation } from './useTranslation/useTranslation'
@ -11,140 +11,16 @@ export function useCopyAs() {
const msg = useTranslation()
return useCallback(
// it's important that this function itself isn't async - we need to
// create the relevant `ClipboardItem`s synchronously to make sure
// safari knows that the user _wants_ to copy:
// https://bugs.webkit.org/show_bug.cgi?id=222262
//
// this is fine for navigator.clipboard.write, but for fallbacks it's a
// little awkward.
function copyAs(ids: TLShapeId[] = editor.getSelectedShapeIds(), format: TLCopyType = 'svg') {
if (ids.length === 0) {
ids = [...editor.currentPageShapeIds]
}
if (ids.length === 0) {
return
}
switch (format) {
case 'svg': {
if (window.navigator.clipboard) {
if (window.navigator.clipboard.write) {
window.navigator.clipboard.write([
new ClipboardItem({
'text/plain': getExportedSvgBlob(editor, ids),
}),
])
} else {
fallbackWriteTextAsync(async () =>
getSvgAsString(await getExportSvgElement(editor, ids))
)
}
}
break
}
case 'jpeg':
case 'png': {
const mimeType = format === 'jpeg' ? 'image/jpeg' : 'image/png'
const blobPromise = getExportedImageBlob(editor, ids, format).then((blob) => {
if (blob) {
if (window.navigator.clipboard) {
return blob
}
throw new Error('Copy not supported')
} else {
addToast({
id: 'copy-fail',
icon: 'warning-triangle',
title: msg('toast.error.copy-fail.title'),
description: msg('toast.error.copy-fail.desc'),
})
throw new Error('Copy not possible')
}
})
window.navigator.clipboard
.write([
new ClipboardItem({
// Note: This needs to use the promise based approach for safari/ios to not bail on a permissions error.
[mimeType]: blobPromise,
}),
])
.catch((err: any) => {
// Firefox will fail with the above if `dom.events.asyncClipboard.clipboardItem` is enabled.
// See <https://github.com/tldraw/tldraw/issues/1325>
if (!err.toString().match(/^TypeError: DOMString not supported/)) {
console.error(err)
}
blobPromise.then((blob) => {
window.navigator.clipboard.write([
new ClipboardItem({
// Note: This needs to use the promise based approach for safari/ios to not bail on a permissions error.
[mimeType]: blob,
}),
])
})
})
break
}
case 'json': {
const data = editor.getContentFromCurrentPage(ids)
if (window.navigator.clipboard) {
const jsonStr = JSON.stringify(data)
if (window.navigator.clipboard.write) {
window.navigator.clipboard.write([
new ClipboardItem({
'text/plain': new Blob([jsonStr], { type: 'text/plain' }),
}),
])
} else {
fallbackWriteTextAsync(async () => jsonStr)
}
}
break
}
default:
throw new Error(`Copy type ${format} not supported.`)
}
(ids: TLShapeId[], format: TLCopyType = 'svg') => {
copyAs(editor, ids, format).catch(() => {
addToast({
id: 'copy-fail',
icon: 'warning-triangle',
title: msg('toast.error.copy-fail.title'),
description: msg('toast.error.copy-fail.desc'),
})
})
},
[editor, addToast, msg]
)
}
async function getExportSvgElement(editor: Editor, ids: TLShapeId[]) {
const svg = await editor.getSvg(ids, {
scale: 1,
background: editor.getInstanceState().exportBackground,
})
if (!svg) throw new Error('Could not construct SVG.')
return svg
}
async function getExportedSvgBlob(editor: Editor, ids: TLShapeId[]) {
return new Blob([getSvgAsString(await getExportSvgElement(editor, ids))], {
type: 'text/plain',
})
}
async function getExportedImageBlob(editor: Editor, ids: TLShapeId[], format: 'png' | 'jpeg') {
return await getSvgAsImage(await getExportSvgElement(editor, ids), editor.environment.isSafari, {
type: format,
quality: 1,
scale: 2,
})
}
async function fallbackWriteTextAsync(getText: () => Promise<string>) {
if (!(navigator && navigator.clipboard)) return
navigator.clipboard.writeText(await getText())
}

View file

@ -1,11 +1,6 @@
import { TLFrameShape, TLShapeId, useEditor } from '@tldraw/editor'
import { TLShapeId, useEditor } from '@tldraw/editor'
import { useCallback } from 'react'
import {
TLExportType,
downloadDataURLAsFile,
getSvgAsDataUrl,
getSvgAsImage,
} from '../../utils/export'
import { TLExportType, exportAs } from '../../utils/export/exportAs'
import { useToasts } from './useToastsProvider'
import { useTranslation } from './useTranslation/useTranslation'
@ -16,97 +11,20 @@ export function useExportAs() {
const msg = useTranslation()
return useCallback(
async function exportAs(
ids: TLShapeId[] = editor.getSelectedShapeIds(),
format: TLExportType = 'png'
) {
if (ids.length === 0) {
ids = [...editor.currentPageShapeIds]
}
if (ids.length === 0) {
return
}
const svg = await editor.getSvg(ids, {
(ids: TLShapeId[], format: TLExportType = 'png') => {
exportAs(editor, ids, format, {
scale: 1,
background: editor.getInstanceState().exportBackground,
background: editor.instanceState.exportBackground,
}).catch((e) => {
console.error(e.message)
addToast({
id: 'export-fail',
// icon: 'error',
title: msg('toast.error.export-fail.title'),
description: msg('toast.error.export-fail.desc'),
})
})
if (!svg) throw new Error('Could not construct SVG.')
let name = 'shapes' + getTimestamp()
if (ids.length === 1) {
const first = editor.getShape(ids[0])!
if (editor.isShapeOfType<TLFrameShape>(first, 'frame')) {
name = first.props.name ?? 'frame'
} else {
name = first.id.replace(/:/, '_')
}
}
switch (format) {
case 'svg': {
const dataURL = await getSvgAsDataUrl(svg)
downloadDataURLAsFile(dataURL, `${name}.svg`)
return
}
case 'webp':
case 'png': {
const image = await getSvgAsImage(svg, editor.environment.isSafari, {
type: format,
quality: 1,
scale: 2,
})
if (!image) {
addToast({
id: 'export-fail',
// icon: 'error',
title: msg('toast.error.export-fail.title'),
description: msg('toast.error.export-fail.desc'),
})
return
}
const dataURL = URL.createObjectURL(image)
downloadDataURLAsFile(dataURL, `${name}.${format}`)
URL.revokeObjectURL(dataURL)
return
}
case 'json': {
const data = editor.getContentFromCurrentPage(ids)
const dataURL = URL.createObjectURL(
new Blob([JSON.stringify(data, null, 4)], { type: 'application/json' })
)
downloadDataURLAsFile(dataURL, `${name || 'shapes'}.json`)
URL.revokeObjectURL(dataURL)
return
}
default:
throw new Error(`Export type ${format} not supported.`)
}
},
[editor, addToast, msg]
)
}
function getTimestamp() {
const now = new Date()
const year = String(now.getFullYear()).slice(2)
const month = String(now.getMonth() + 1).padStart(2, '0')
const day = String(now.getDate()).padStart(2, '0')
const hours = String(now.getHours()).padStart(2, '0')
const minutes = String(now.getMinutes()).padStart(2, '0')
const seconds = String(now.getSeconds()).padStart(2, '0')
return ` at ${year}-${month}-${day} ${hours}.${minutes}.${seconds}`
}

View file

@ -1,6 +1,6 @@
import { Editor, TLBookmarkShape, TLEmbedShape, useEditor, useValue } from '@tldraw/editor'
import React, { useMemo } from 'react'
import { getEmbedInfo } from '../../utils/embeds'
import { getEmbedInfo } from '../../utils/embeds/embeds'
import {
TLUiMenuSchema,
compactMenuItems,
@ -111,7 +111,7 @@ export function TLUiMenuSchemaProvider({ overrides, children }: TLUiMenuSchemaPr
)
const menuSchema = useMemo<TLUiMenuSchema>(() => {
const menuSchema = compactMenuItems([
const menuSchema: TLUiMenuSchema = compactMenuItems([
menuGroup(
'menu',
menuSubmenu(

View file

@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react'
import { TLEditorAssetUrls } from './assetUrls'
import { TLEditorAssetUrls } from '../../utils/static-assets/assetUrls'
export type TLTypeFace = {
url: string

View file

@ -69,9 +69,9 @@ export function ToolbarSchemaProvider({ overrides, children }: TLUiToolbarSchema
toolbarItem(tools['arrow-up']),
toolbarItem(tools['arrow-down']),
toolbarItem(tools['arrow-right']),
toolbarItem(tools.frame),
toolbarItem(tools.line),
toolbarItem(tools.highlight),
toolbarItem(tools.frame),
toolbarItem(tools.laser),
])

View file

@ -8,11 +8,14 @@ import { useInsertMedia } from './useInsertMedia'
import { TLUiTranslationKey } from './useTranslation/TLUiTranslationKey'
/** @public */
export interface TLUiToolItem {
export interface TLUiToolItem<
TranslationKey extends string = string,
IconType extends string = string
> {
id: string
label: TLUiTranslationKey
shortcutsLabel?: TLUiTranslationKey
icon: TLUiIconType
label: TranslationKey
shortcutsLabel?: TranslationKey
icon: IconType
onSelect: (source: TLUiEventSource) => void
kbd?: string
readonlyOk: boolean
@ -46,7 +49,7 @@ export function ToolsProvider({ overrides, children }: TLUiToolsProviderProps) {
const insertMedia = useInsertMedia()
const tools = React.useMemo<TLUiToolsContextType>(() => {
const toolsArray: TLUiToolItem[] = [
const toolsArray: TLUiToolItem<TLUiTranslationKey, TLUiIconType>[] = [
{
id: 'select',
label: 'tool.select',

View file

@ -105,8 +105,8 @@ export const TranslationProvider = track(function TranslationProvider({
export function useTranslation() {
const translation = useCurrentTranslation()
return React.useCallback(
function msg(id: TLUiTranslationKey) {
return translation.messages[id] ?? id
function msg(id: Exclude<string, TLUiTranslationKey> | string) {
return translation.messages[id as TLUiTranslationKey] ?? id
},
[translation]
)

View file

@ -58,31 +58,31 @@ type WithDefaultHelpers<T extends TLUiOverride<any, any>> = T extends TLUiOverri
: never
/** @public */
export interface TLUiOverrides {
actionsMenu?: WithDefaultHelpers<NonNullable<ActionsMenuSchemaProviderProps['overrides']>>
actions?: WithDefaultHelpers<NonNullable<ActionsProviderProps['overrides']>>
contextMenu?: WithDefaultHelpers<NonNullable<TLUiContextMenuSchemaProviderProps['overrides']>>
helpMenu?: WithDefaultHelpers<NonNullable<TLUiHelpMenuSchemaProviderProps['overrides']>>
menu?: WithDefaultHelpers<NonNullable<TLUiMenuSchemaProviderProps['overrides']>>
toolbar?: WithDefaultHelpers<NonNullable<TLUiToolbarSchemaProviderProps['overrides']>>
keyboardShortcutsMenu?: WithDefaultHelpers<
export type TLUiOverrides = Partial<{
actionsMenu: WithDefaultHelpers<NonNullable<ActionsMenuSchemaProviderProps['overrides']>>
actions: WithDefaultHelpers<NonNullable<ActionsProviderProps['overrides']>>
contextMenu: WithDefaultHelpers<NonNullable<TLUiContextMenuSchemaProviderProps['overrides']>>
helpMenu: WithDefaultHelpers<NonNullable<TLUiHelpMenuSchemaProviderProps['overrides']>>
menu: WithDefaultHelpers<NonNullable<TLUiMenuSchemaProviderProps['overrides']>>
toolbar: WithDefaultHelpers<NonNullable<TLUiToolbarSchemaProviderProps['overrides']>>
keyboardShortcutsMenu: WithDefaultHelpers<
NonNullable<TLUiKeyboardShortcutsSchemaProviderProps['overrides']>
>
tools?: WithDefaultHelpers<NonNullable<TLUiToolsProviderProps['overrides']>>
translations?: TLUiTranslationProviderProps['overrides']
}
tools: WithDefaultHelpers<NonNullable<TLUiToolsProviderProps['overrides']>>
translations: TLUiTranslationProviderProps['overrides']
}>
export interface TLUiOverridesWithoutDefaults {
actionsMenu?: ActionsMenuSchemaProviderProps['overrides']
actions?: ActionsProviderProps['overrides']
contextMenu?: TLUiContextMenuSchemaProviderProps['overrides']
helpMenu?: TLUiHelpMenuSchemaProviderProps['overrides']
menu?: TLUiMenuSchemaProviderProps['overrides']
toolbar?: TLUiToolbarSchemaProviderProps['overrides']
keyboardShortcutsMenu?: TLUiKeyboardShortcutsSchemaProviderProps['overrides']
tools?: TLUiToolsProviderProps['overrides']
translations?: TLUiTranslationProviderProps['overrides']
}
export type TLUiOverridesWithoutDefaults = Partial<{
actionsMenu: ActionsMenuSchemaProviderProps['overrides']
actions: ActionsProviderProps['overrides']
contextMenu: TLUiContextMenuSchemaProviderProps['overrides']
helpMenu: TLUiHelpMenuSchemaProviderProps['overrides']
menu: TLUiMenuSchemaProviderProps['overrides']
toolbar: TLUiToolbarSchemaProviderProps['overrides']
keyboardShortcutsMenu: TLUiKeyboardShortcutsSchemaProviderProps['overrides']
tools: TLUiToolsProviderProps['overrides']
translations: TLUiTranslationProviderProps['overrides']
}>
export function mergeOverrides(
overrides: TLUiOverrides[],

View file

@ -1,6 +1,6 @@
import downscale from 'downscale'
import { getBrowserCanvasMaxSize } from '../shapes/shared/getBrowserCanvasMaxSize'
import { isAnimated } from './is-gif-animated'
import { isAnimated } from './assets/is-gif-animated'
type BoxWidthHeight = {
w: number

Some files were not shown because too many files have changed in this diff Show more