Example of using tldraw styles (#3017)
Adds an example of how to use tldraw styles in a custom shape - [x] `documentation` — Changes to the documentation only[^2] ### Release Notes - shape with tldraw styles example --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
parent
338501d656
commit
66a8b0a4a6
9 changed files with 316 additions and 346 deletions
|
@ -1,141 +0,0 @@
|
||||||
import {
|
|
||||||
BaseBoxShapeTool,
|
|
||||||
BaseBoxShapeUtil,
|
|
||||||
DefaultColorStyle,
|
|
||||||
HTMLContainer,
|
|
||||||
StyleProp,
|
|
||||||
T,
|
|
||||||
TLBaseShape,
|
|
||||||
TLDefaultColorStyle,
|
|
||||||
getDefaultColorTheme,
|
|
||||||
} from 'tldraw'
|
|
||||||
|
|
||||||
// There's a guide at the bottom of this file!
|
|
||||||
|
|
||||||
// [1]
|
|
||||||
export const MyFilterStyle = StyleProp.defineEnum('myApp:filter', {
|
|
||||||
defaultValue: 'none',
|
|
||||||
values: ['none', 'invert', 'grayscale', 'blur'],
|
|
||||||
})
|
|
||||||
|
|
||||||
export type MyFilterStyle = T.TypeOf<typeof MyFilterStyle>
|
|
||||||
|
|
||||||
// [2]
|
|
||||||
export type CardShape = TLBaseShape<
|
|
||||||
'card',
|
|
||||||
{
|
|
||||||
w: number
|
|
||||||
h: number
|
|
||||||
color: TLDefaultColorStyle
|
|
||||||
filter: MyFilterStyle
|
|
||||||
}
|
|
||||||
>
|
|
||||||
|
|
||||||
//[3]
|
|
||||||
export class CardShapeUtil extends BaseBoxShapeUtil<CardShape> {
|
|
||||||
static override type = 'card' as const
|
|
||||||
|
|
||||||
//[a]
|
|
||||||
static override props = {
|
|
||||||
w: T.number,
|
|
||||||
h: T.number,
|
|
||||||
color: DefaultColorStyle,
|
|
||||||
filter: MyFilterStyle,
|
|
||||||
}
|
|
||||||
|
|
||||||
override isAspectRatioLocked = (_shape: CardShape) => false
|
|
||||||
override canResize = (_shape: CardShape) => true
|
|
||||||
override canBind = (_shape: CardShape) => true
|
|
||||||
|
|
||||||
override getDefaultProps(): CardShape['props'] {
|
|
||||||
return {
|
|
||||||
w: 300,
|
|
||||||
h: 300,
|
|
||||||
color: 'black',
|
|
||||||
filter: 'none',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// [b]
|
|
||||||
component(shape: CardShape) {
|
|
||||||
const bounds = this.editor.getShapeGeometry(shape).bounds
|
|
||||||
const theme = getDefaultColorTheme({ isDarkMode: this.editor.user.getIsDarkMode() })
|
|
||||||
|
|
||||||
return (
|
|
||||||
<HTMLContainer
|
|
||||||
id={shape.id}
|
|
||||||
style={{
|
|
||||||
border: `4px solid ${theme[shape.props.color].solid}`,
|
|
||||||
borderRadius: 4,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
pointerEvents: 'all',
|
|
||||||
filter: this.filterStyleToCss(shape.props.filter),
|
|
||||||
backgroundColor: theme[shape.props.color].semi,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
🍇🫐🍏🍋🍊🍒 {bounds.w.toFixed()}x{bounds.h.toFixed()} 🍒🍊🍋🍏🫐🍇
|
|
||||||
</HTMLContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
indicator(shape: CardShape) {
|
|
||||||
return <rect width={shape.props.w} height={shape.props.h} />
|
|
||||||
}
|
|
||||||
|
|
||||||
//[c]
|
|
||||||
filterStyleToCss(filter: MyFilterStyle) {
|
|
||||||
if (filter === 'invert') return 'invert(100%)'
|
|
||||||
if (filter === 'grayscale') return 'grayscale(100%)'
|
|
||||||
if (filter === 'blur') return 'blur(10px)'
|
|
||||||
return 'none'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// [4]
|
|
||||||
export class CardShapeTool extends BaseBoxShapeTool {
|
|
||||||
static override id = 'card'
|
|
||||||
static override initial = 'idle'
|
|
||||||
override shapeType = 'card'
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
Introduction:
|
|
||||||
This file contains the logic for how the custom shape and style work. This guide will
|
|
||||||
mostly focus on the custom style features, for a more in-depth look at creating a custom
|
|
||||||
shape check out the custom shapes/tools example. For a closer look at creating more
|
|
||||||
custom tool interactions, checkout out the screenshot example.
|
|
||||||
|
|
||||||
|
|
||||||
[1]
|
|
||||||
This is where we define our custom style. We use the `StyleProp.defineEnum` method to
|
|
||||||
define an enum style. This will create a style that can be one of the values we pass
|
|
||||||
in to the `values` property. We also pass in a `defaultValue` property, this will be
|
|
||||||
the default value for the style. It's important that the StyleProp is unique, so we
|
|
||||||
reccomend prefixing it with your app name.
|
|
||||||
|
|
||||||
[2]
|
|
||||||
Defining our shape's type. Here we import a type for color, the default tldraw style,
|
|
||||||
and also use our own type for our custom style: filter.
|
|
||||||
|
|
||||||
[3]
|
|
||||||
This is our util, where we define the logic for our shape, it's geometry, resize behaviour
|
|
||||||
and render method.
|
|
||||||
- [a] The props for our shape. We can import a validator for the default color style
|
|
||||||
and use our own for our custom style.
|
|
||||||
- [b] The render method for our custom shape, this is where we tell the browser how
|
|
||||||
how to render our different styles using the style attribute. Using the
|
|
||||||
getDefaultColorTheme function along with the getIsDarkMode method gives us access
|
|
||||||
to the tldraw default colorsand ensures they stay up to date when switching between
|
|
||||||
light and dark mode.
|
|
||||||
We apply our filter style using a method we've defined on the shape util called
|
|
||||||
filterStyleToCss
|
|
||||||
- [c] This is our method for converting the style the user selected into CSS
|
|
||||||
|
|
||||||
Check out FilterStyleUi.tsx to see how we render the UI for our custom style.
|
|
||||||
|
|
||||||
[4]
|
|
||||||
This is our tool, it's very simple, we just define the id and initial state. Extending the
|
|
||||||
BaseBoxShapeTool gives us a lot of the default behaviour for free.
|
|
||||||
|
|
||||||
*/
|
|
|
@ -1,55 +0,0 @@
|
||||||
import { Tldraw } from 'tldraw'
|
|
||||||
import 'tldraw/tldraw.css'
|
|
||||||
import { CardShapeTool, CardShapeUtil } from './CardShape'
|
|
||||||
import { FilterStyleUi } from './FilterStyleUi'
|
|
||||||
import { components, uiOverrides } from './ui-overrides'
|
|
||||||
|
|
||||||
// There's a guide at the bottom of this file!
|
|
||||||
|
|
||||||
// [1]
|
|
||||||
const customShapeUtils = [CardShapeUtil]
|
|
||||||
const customTools = [CardShapeTool]
|
|
||||||
|
|
||||||
// [2]
|
|
||||||
export default function CustomStylesExample() {
|
|
||||||
return (
|
|
||||||
<div className="tldraw__editor">
|
|
||||||
<Tldraw
|
|
||||||
persistenceKey="custom-styles-example"
|
|
||||||
shapeUtils={customShapeUtils}
|
|
||||||
tools={customTools}
|
|
||||||
overrides={uiOverrides}
|
|
||||||
components={components}
|
|
||||||
>
|
|
||||||
<FilterStyleUi />
|
|
||||||
</Tldraw>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
Introduction:
|
|
||||||
|
|
||||||
This example shows how to create your own custom styles to use with your shapes.
|
|
||||||
It also shows how to create a very simple ui for your styles. In this example, we
|
|
||||||
create a custom style for a card shape that lets the user apply a filter to blur,
|
|
||||||
invert or grayscale the card.
|
|
||||||
|
|
||||||
[1]
|
|
||||||
We define an array to hold the custom shape util and custom tool. It's important to
|
|
||||||
do this outside of any React component so that this array doesn't get redefined on
|
|
||||||
every render. We'll pass this into the Tldraw component's `shapeUtils` and `tools`
|
|
||||||
props.
|
|
||||||
|
|
||||||
Check out CardShape.tsx to see how we define the shape util, tool and the custom
|
|
||||||
style.
|
|
||||||
|
|
||||||
[2]
|
|
||||||
We pass the custom shape util and tool into the Tldraw component's `shapeUtils` and
|
|
||||||
`tools` props. We also pass in the custom ui overrides, this will make an icon for
|
|
||||||
our shape/tool appear on the toolbar (see ui-overrides.ts). And render our
|
|
||||||
FilterStyleUi component inside the Tldraw component.
|
|
||||||
|
|
||||||
Check out FilterStyleUi.tsx to see how we render this Ui only when the user has
|
|
||||||
selected a shape that uses the custom style.
|
|
||||||
|
|
||||||
*/
|
|
|
@ -1,80 +0,0 @@
|
||||||
import { track, useEditor } from 'tldraw'
|
|
||||||
import { MyFilterStyle } from './CardShape'
|
|
||||||
|
|
||||||
// There's a guide at the bottom of this file!
|
|
||||||
|
|
||||||
//[1]
|
|
||||||
export const FilterStyleUi = track(function FilterStyleUi() {
|
|
||||||
const editor = useEditor()
|
|
||||||
//[2]
|
|
||||||
const filterStyle = editor.getSharedStyles().get(MyFilterStyle)
|
|
||||||
if (!filterStyle) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="tlui-style-panel__wrapper"
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
zIndex: 300,
|
|
||||||
top: 50,
|
|
||||||
left: 8,
|
|
||||||
padding: 15,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
filter:{' '}
|
|
||||||
<select
|
|
||||||
value={filterStyle.type === 'mixed' ? 'mixed' : filterStyle.value}
|
|
||||||
// [3]
|
|
||||||
onChange={(e) => {
|
|
||||||
editor.batch(() => {
|
|
||||||
if (editor.isIn('select')) {
|
|
||||||
editor.setStyleForSelectedShapes(
|
|
||||||
MyFilterStyle,
|
|
||||||
MyFilterStyle.validate(e.target.value)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
editor.setStyleForNextShapes(MyFilterStyle, MyFilterStyle.validate(e.target.value))
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<option value="mixed" disabled>
|
|
||||||
Mixed
|
|
||||||
</option>
|
|
||||||
<option value="none">None</option>
|
|
||||||
<option value="invert">Invert</option>
|
|
||||||
<option value="grayscale">Grayscale</option>
|
|
||||||
<option value="blur">Blur</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
/*
|
|
||||||
Introduction:
|
|
||||||
This is a an example of how to create a custom ui for your custom style. We want
|
|
||||||
to render the UI when the user has selected our card tool, or when they've selected a card
|
|
||||||
shape. Here, we've chosen a drop-down to let the user select the filter type, and we render
|
|
||||||
it in the top left corner of the editor. You could render your UI anywhere you want. Check
|
|
||||||
out the zones example to see how to render your UI in a particular zone, or the custom-ui
|
|
||||||
example if you want to redo the entire ui.
|
|
||||||
|
|
||||||
[1]
|
|
||||||
We use the `track` function to wrap our component. This makes our component reactive- it will
|
|
||||||
re-render whenever the signals it is tracking change. Check out the signia docs for more:
|
|
||||||
https://signia.tldraw.dev/docs/API/signia_react/functions/track
|
|
||||||
|
|
||||||
[2]
|
|
||||||
Here we check if the user has selected a shape that uses our custom style, or if they've
|
|
||||||
selected a tool associated with our custom style. If they haven't, we return null and don't
|
|
||||||
render anything.
|
|
||||||
|
|
||||||
[3]
|
|
||||||
Here we add an event handler for when the user changes the value of the dropdown. We use the
|
|
||||||
`batch` method to batch our changes into a single undoable action. We check if the user has
|
|
||||||
selected any shapes, and if they have, we set the style for those shapes. We also set the style
|
|
||||||
for any shapes the user creates next.
|
|
||||||
|
|
||||||
*/
|
|
|
@ -1,12 +0,0 @@
|
||||||
---
|
|
||||||
title: Custom styles
|
|
||||||
component: ./CustomStylesExample.tsx
|
|
||||||
category: shapes/tools
|
|
||||||
priority: 2
|
|
||||||
---
|
|
||||||
|
|
||||||
Styles are special properties that can be set on many shapes at once.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Create several shapes with the ⚫️ tool, then select them and try changing their filter style.
|
|
|
@ -1,58 +0,0 @@
|
||||||
import {
|
|
||||||
DefaultKeyboardShortcutsDialog,
|
|
||||||
DefaultKeyboardShortcutsDialogContent,
|
|
||||||
TLComponents,
|
|
||||||
TLUiOverrides,
|
|
||||||
TldrawUiMenuItem,
|
|
||||||
toolbarItem,
|
|
||||||
useTools,
|
|
||||||
} from 'tldraw'
|
|
||||||
|
|
||||||
// There's a guide at the bottom of this file!
|
|
||||||
|
|
||||||
export const uiOverrides: TLUiOverrides = {
|
|
||||||
tools(editor, tools) {
|
|
||||||
tools.card = {
|
|
||||||
id: 'card',
|
|
||||||
icon: 'color',
|
|
||||||
label: 'Card' as any,
|
|
||||||
kbd: 'c',
|
|
||||||
onSelect: () => {
|
|
||||||
editor.setCurrentTool('card')
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return tools
|
|
||||||
},
|
|
||||||
toolbar(_app, toolbar, { tools }) {
|
|
||||||
toolbar.splice(4, 0, toolbarItem(tools.card))
|
|
||||||
return toolbar
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export const components: TLComponents = {
|
|
||||||
KeyboardShortcutsDialog: (props) => {
|
|
||||||
const tools = useTools()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DefaultKeyboardShortcutsDialog {...props}>
|
|
||||||
<DefaultKeyboardShortcutsDialogContent />
|
|
||||||
{/* Ideally, we'd interleave this into the tools section */}
|
|
||||||
<TldrawUiMenuItem {...tools['card']} />
|
|
||||||
</DefaultKeyboardShortcutsDialog>
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
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.
|
|
||||||
|
|
||||||
For this example the icon we use is the same as the color icon. For an example
|
|
||||||
of how to add a custom icon, see the screenshot or speech-bubble examples.
|
|
||||||
|
|
||||||
*/
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
---
|
||||||
|
title: Using custom styles
|
||||||
|
component: ./ShapeWithCustomStylesExample.tsx
|
||||||
|
category: shapes/tools
|
||||||
|
priority: 2
|
||||||
|
---
|
||||||
|
|
||||||
|
Using the custom styles API with your custom shapes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
In the Using tldraw styles example, we showed how to use tldraw's default styles in your own shapes. This example shows how to create your own styles and use them in your own shapes.
|
|
@ -0,0 +1,168 @@
|
||||||
|
import {
|
||||||
|
BaseBoxShapeUtil,
|
||||||
|
DefaultStylePanel,
|
||||||
|
DefaultStylePanelContent,
|
||||||
|
HTMLContainer,
|
||||||
|
StyleProp,
|
||||||
|
T,
|
||||||
|
TLBaseShape,
|
||||||
|
Tldraw,
|
||||||
|
useEditor,
|
||||||
|
useRelevantStyles,
|
||||||
|
} from 'tldraw'
|
||||||
|
import 'tldraw/tldraw.css'
|
||||||
|
|
||||||
|
// [1]
|
||||||
|
const myRatingStyle = StyleProp.defineEnum('example:rating', {
|
||||||
|
defaultValue: 1,
|
||||||
|
values: [1, 2, 3, 4, 5],
|
||||||
|
})
|
||||||
|
|
||||||
|
// [2]
|
||||||
|
type MyRatingStyle = T.TypeOf<typeof myRatingStyle>
|
||||||
|
|
||||||
|
type IMyShape = TLBaseShape<
|
||||||
|
'myshape',
|
||||||
|
{
|
||||||
|
w: number
|
||||||
|
h: number
|
||||||
|
rating: MyRatingStyle
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
class MyShapeUtil extends BaseBoxShapeUtil<IMyShape> {
|
||||||
|
static override type = 'myshape' as const
|
||||||
|
|
||||||
|
// [3]
|
||||||
|
static override props = {
|
||||||
|
w: T.number,
|
||||||
|
h: T.number,
|
||||||
|
rating: myRatingStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultProps(): IMyShape['props'] {
|
||||||
|
return {
|
||||||
|
w: 300,
|
||||||
|
h: 300,
|
||||||
|
rating: 4, // [4]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component(shape: IMyShape) {
|
||||||
|
// [5]
|
||||||
|
const stars = ['☆', '☆', '☆', '☆', '☆']
|
||||||
|
for (let i = 0; i < shape.props.rating; i++) {
|
||||||
|
stars[i] = '★'
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HTMLContainer
|
||||||
|
id={shape.id}
|
||||||
|
style={{ backgroundColor: 'var(--color-low-border)', overflow: 'hidden' }}
|
||||||
|
>
|
||||||
|
{stars}
|
||||||
|
</HTMLContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator(shape: IMyShape) {
|
||||||
|
return <rect width={shape.props.w} height={shape.props.h} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// [6]
|
||||||
|
function CustomStylePanel() {
|
||||||
|
const editor = useEditor()
|
||||||
|
const styles = useRelevantStyles()
|
||||||
|
if (!styles) return null
|
||||||
|
|
||||||
|
const rating = styles.get(myRatingStyle)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DefaultStylePanel>
|
||||||
|
<DefaultStylePanelContent styles={styles} />
|
||||||
|
{rating !== undefined && (
|
||||||
|
<div>
|
||||||
|
<select
|
||||||
|
style={{ width: '100%', padding: 4 }}
|
||||||
|
value={rating.type === 'mixed' ? '' : rating.value}
|
||||||
|
onChange={(e) => {
|
||||||
|
editor.mark('changing rating')
|
||||||
|
const value = myRatingStyle.validate(+e.currentTarget.value)
|
||||||
|
editor.setStyleForSelectedShapes(myRatingStyle, value)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{rating.type === 'mixed' ? <option value="">Mixed</option> : null}
|
||||||
|
<option value={1}>1</option>
|
||||||
|
<option value={2}>2</option>
|
||||||
|
<option value={3}>3</option>
|
||||||
|
<option value={4}>4</option>
|
||||||
|
<option value={5}>5</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DefaultStylePanel>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ShapeWithTldrawStylesExample() {
|
||||||
|
return (
|
||||||
|
<div className="tldraw__editor">
|
||||||
|
<Tldraw
|
||||||
|
// [7]
|
||||||
|
shapeUtils={[MyShapeUtil]}
|
||||||
|
components={{
|
||||||
|
StylePanel: CustomStylePanel,
|
||||||
|
}}
|
||||||
|
onMount={(editor) => {
|
||||||
|
editor.createShape({ type: 'myshape', x: 100, y: 100 })
|
||||||
|
editor.selectAll()
|
||||||
|
editor.createShape({ type: 'myshape', x: 450, y: 250, props: { rating: 5 } })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
This file shows a custom shape that uses a user-created styles
|
||||||
|
|
||||||
|
For more on custom shapes, see our Custom Shape example.
|
||||||
|
|
||||||
|
[1]
|
||||||
|
In this example, our custom shape will use a new style called "rating".
|
||||||
|
We'll need to create the style so that we can pass it to the shape's props.
|
||||||
|
|
||||||
|
[2]
|
||||||
|
Here's we extract the type of the style's values. We use it below when
|
||||||
|
we define the shape's props.
|
||||||
|
|
||||||
|
[3]
|
||||||
|
We pass the style to the shape's props.
|
||||||
|
|
||||||
|
[4]
|
||||||
|
Since this property uses one a style, whatever value we put here in the
|
||||||
|
shape's default props will be overwritten by the editor's current value
|
||||||
|
for that style, which will either be the default value or the most
|
||||||
|
recent value the user has set. This is special behavior just for styles.
|
||||||
|
|
||||||
|
[5]
|
||||||
|
We can use the styles in the component just like any other prop.
|
||||||
|
|
||||||
|
[6]
|
||||||
|
Here we create a custom style panel that includes the default style panel
|
||||||
|
and also a dropdown for the rating style. We use the useRelevantStyles hook
|
||||||
|
to get the styles of the user's selected shapes, and the useEditor hook to
|
||||||
|
set the style for the selected shapes. For more on customizing the style
|
||||||
|
panel, see our custom style panel example.
|
||||||
|
|
||||||
|
[7]
|
||||||
|
We pass the custom shape util and custom components in as props.
|
||||||
|
|
||||||
|
[8]
|
||||||
|
And for this example, we create two shapes: the first does not specify a
|
||||||
|
rating, so it will use the editor's current style value (in this example,
|
||||||
|
this will be the style's default value of 4). The second specifies a
|
||||||
|
rating of 5, so it will use that value.
|
||||||
|
*/
|
|
@ -0,0 +1,16 @@
|
||||||
|
---
|
||||||
|
title: Using tldraw styles
|
||||||
|
component: ./ShapeWithTldrawStylesExample.tsx
|
||||||
|
category: shapes/tools
|
||||||
|
priority: 1
|
||||||
|
---
|
||||||
|
|
||||||
|
Using the tldraw style panel with your custom shapes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
The default tldraw UI will displaye UI for the styles of your selection or your current tool. For example, when you have two shapes selected that both have the tldraw's "size" style, the size selector will be displayed. If all of your selected shapes have the same value for this style, that value will be shown as selected in the panel. If they have different values, the panel will show the value as "mixed".
|
||||||
|
|
||||||
|
You can use tldraw's Styles API to create your own styles that behave in the same way, though you'll also need to create a custom UI for your style.
|
||||||
|
|
||||||
|
Alternatively, you can use tldraw's default styles in your own shapes. This example shows how to do that.
|
|
@ -0,0 +1,120 @@
|
||||||
|
import {
|
||||||
|
BaseBoxShapeUtil,
|
||||||
|
DefaultColorStyle,
|
||||||
|
DefaultSizeStyle,
|
||||||
|
HTMLContainer,
|
||||||
|
T,
|
||||||
|
TLBaseShape,
|
||||||
|
TLDefaultColorStyle,
|
||||||
|
TLDefaultSizeStyle,
|
||||||
|
Tldraw,
|
||||||
|
} from 'tldraw'
|
||||||
|
import { useDefaultColorTheme } from 'tldraw/src/lib/shapes/shared/ShapeFill'
|
||||||
|
import 'tldraw/tldraw.css'
|
||||||
|
|
||||||
|
// There's a guide at the bottom of this file!
|
||||||
|
|
||||||
|
const FONT_SIZES: Record<TLDefaultSizeStyle, number> = {
|
||||||
|
s: 14,
|
||||||
|
m: 25,
|
||||||
|
l: 38,
|
||||||
|
xl: 48,
|
||||||
|
}
|
||||||
|
|
||||||
|
type IMyShape = TLBaseShape<
|
||||||
|
'myshape',
|
||||||
|
{
|
||||||
|
w: number
|
||||||
|
h: number
|
||||||
|
// [1]
|
||||||
|
size: TLDefaultSizeStyle
|
||||||
|
color: TLDefaultColorStyle
|
||||||
|
}
|
||||||
|
>
|
||||||
|
|
||||||
|
class MyShapeUtil extends BaseBoxShapeUtil<IMyShape> {
|
||||||
|
static override type = 'myshape' as const
|
||||||
|
|
||||||
|
// [2]
|
||||||
|
static override props = {
|
||||||
|
w: T.number,
|
||||||
|
h: T.number,
|
||||||
|
size: DefaultSizeStyle,
|
||||||
|
color: DefaultColorStyle,
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultProps(): IMyShape['props'] {
|
||||||
|
return {
|
||||||
|
w: 300,
|
||||||
|
h: 300,
|
||||||
|
size: 'm',
|
||||||
|
color: 'black',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component(shape: IMyShape) {
|
||||||
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||||
|
const theme = useDefaultColorTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HTMLContainer
|
||||||
|
id={shape.id}
|
||||||
|
style={{ backgroundColor: 'var(--color-low-border)', overflow: 'hidden' }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
// [3]
|
||||||
|
fontSize: FONT_SIZES[shape.props.size],
|
||||||
|
color: theme[shape.props.color].solid,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Select the shape and use the style panel to change the font size and color
|
||||||
|
</div>
|
||||||
|
</HTMLContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator(shape: IMyShape) {
|
||||||
|
return <rect width={shape.props.w} height={shape.props.h} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const customShapeUtils = [MyShapeUtil]
|
||||||
|
|
||||||
|
export default function ShapeWithTldrawStylesExample() {
|
||||||
|
return (
|
||||||
|
<div className="tldraw__editor">
|
||||||
|
<Tldraw
|
||||||
|
shapeUtils={customShapeUtils}
|
||||||
|
onMount={(editor) => {
|
||||||
|
editor.createShape({ type: 'myshape', x: 100, y: 100 })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
This file shows a custom shape that uses tldraw's default styles.
|
||||||
|
For more on custom shapes, see our Custom Shape example.
|
||||||
|
|
||||||
|
[1]
|
||||||
|
In this example, our custom shape will use the size and color styles from the
|
||||||
|
default styles. When typing a custom shape, you can use our types for
|
||||||
|
these styles.
|
||||||
|
|
||||||
|
[2]
|
||||||
|
For the shape's props, we'll pass the DefaultSizeStyle and DefaultColorStyle
|
||||||
|
styles for the two properties, size and color. There's nothing special about
|
||||||
|
these styles except that the editor will notice when two shapes are selected
|
||||||
|
that share the same style. (You can use the useRelevantStyles hook to get the
|
||||||
|
styles of the user's selected shapes.)
|
||||||
|
|
||||||
|
[3]
|
||||||
|
Here in the component, we'll use the styles to change the way that our shape
|
||||||
|
appears. The style values themselves are just strings, like 'xl' or 'black',
|
||||||
|
so it's up to you to decide how to use them. In this example, we're using the
|
||||||
|
size to set the text's font-size property, and also using the default theme
|
||||||
|
(via the useDefaultColorTheme hook) to get the color for the text.
|
||||||
|
*/
|
Loading…
Reference in a new issue