Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

textfields [2 of 3]: refactor to share logic/components; make composable UI #3051

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
"dependencies": {
"@playwright/test": "^1.38.1",
"@radix-ui/react-alert-dialog": "^1.0.5",
"@tiptap/core": "^2.2.4",
"@tiptap/pm": "^2.2.4",
"@tiptap/react": "^2.2.4",
"@tiptap/starter-kit": "^2.2.4",
"@tldraw/assets": "workspace:*",
"@vercel/analytics": "^1.1.1",
"classnames": "^2.3.2",
Expand Down
45 changes: 45 additions & 0 deletions apps/examples/src/examples/custom-rich-text/CustomRichText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { TLComponents, TLTextLabelProps, Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
import Tiptap from '../../shared/TipTap'

function CustomRichText({
id,
type,
text,
labelColor,
font,
fontSize,
lineHeight,
align,
verticalAlign,
wrap,
bounds,
}: TLTextLabelProps) {
return (
<Tiptap
id={id}
type={type}
labelColor={labelColor}
font={font}
fontSize={fontSize}
lineHeight={lineHeight}
align={align}
verticalAlign={verticalAlign}
content={text}
wrap={wrap}
bounds={bounds}
/>
)
}

const components: TLComponents = {
TextLabel: CustomRichText,
}

export default function CustomRichTextExample() {
return (
<div className="tldraw__editor">
<Tldraw components={components} />
</div>
)
}
12 changes: 12 additions & 0 deletions apps/examples/src/examples/custom-rich-text/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
title: Rich text
component: ./CustomRichText.tsx
category: shapes/tools
priority: 0
---

You can customize tldraw's text label.

---

The text label can be customized by providing a `TextLabel` component to the `Tldraw` component's `components` prop.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
import { SpeechBubbleTool } from '../speech-bubble/SpeechBubble/SpeechBubbleTool'
import {
components,
customAssetUrls,
uiOverrides,
} from '../speech-bubble/SpeechBubble/ui-overrides'
import '../speech-bubble/customhandles.css'
import { SpeechBubbleUtilRichText } from './SpeechBubbleUtilRichText'

// There's a guide at the bottom of this file!

// [1]
const shapeUtils = [SpeechBubbleUtilRichText]
const tools = [SpeechBubbleTool]

// [2]
export default function CustomShapeRichText() {
return (
<div style={{ position: 'absolute', inset: 0 }}>
<Tldraw
shapeUtils={shapeUtils}
tools={tools}
overrides={uiOverrides}
assetUrls={customAssetUrls}
components={components}
persistenceKey="whatever-rich-text"
/>
</div>
)
}

/*
For this particular guide check out the main `speech-bubble` example.
This is the same guide except this utilizes SpeechBubbleUtilRichText to add rich text to the shape.
*/
8 changes: 8 additions & 0 deletions apps/examples/src/examples/speech-bubble-rich-text/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
title: Speech bubble with rich text
component: ./CustomShapeRichText.tsx
category: shapes/tools
priority: 0
---

A custom shape with handles and rich text.
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { LABEL_FONT_SIZES, TEXT_PROPS, TLDefaultSizeStyle, getDefaultColorTheme } from 'tldraw'
import Tiptap from '../../shared/TipTap'
import type {
SpeechBubbleShape,
SpeechBubbleShapeProps,
} from '../speech-bubble/SpeechBubble/SpeechBubbleUtil'
import { STROKE_SIZES, SpeechBubbleUtil } from '../speech-bubble/SpeechBubble/SpeechBubbleUtil'
import { getSpeechBubbleVertices } from '../speech-bubble/SpeechBubble/helpers'

export class SpeechBubbleUtilRichText extends SpeechBubbleUtil {
override getDefaultProps(): SpeechBubbleShapeProps {
return { ...super.getDefaultProps(), text: '<p>Hello <b>World</b>! Some <i>rich</i> text</p>' }
}

override component(shape: SpeechBubbleShape) {
const {
id,
type,
props: { color, font, size, align, text },
} = shape
const theme = getDefaultColorTheme({
isDarkMode: this.editor.user.getIsDarkMode(),
})
const vertices = getSpeechBubbleVertices(shape)
const pathData = 'M' + vertices[0] + 'L' + vertices.slice(1) + 'Z'

return (
<>
<svg className="tl-svg-container">
<path
d={pathData}
strokeWidth={STROKE_SIZES[size]}
stroke={theme[color].solid}
fill={'none'}
/>
</svg>

<div style={{ padding: '1rem' }}>
<Tiptap
id={id}
type={type}
labelColor={color}
font={font}
fontSize={LABEL_FONT_SIZES[size as TLDefaultSizeStyle]}
lineHeight={TEXT_PROPS.lineHeight}
align={align}
verticalAlign="start"
content={text}
/>
</div>
</>
)
}
}

/*
For this particular guide check out the main `speech-bubble` example.
This is the same guide except this adds rich text to the shape using TipTap.
*/
2 changes: 1 addition & 1 deletion apps/examples/src/examples/speech-bubble/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: Speech bubble
component: ./CustomShapeWithHandles.tsx
category: shapes/tools
priority: 2
priority: 0
---

A custom shape with handles
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
import { ShapePropsType } from '@tldraw/tlschema/src/shapes/TLBaseShape'
import {
DefaultColorStyle,
DefaultFontStyle,
DefaultHorizontalAlignStyle,
DefaultSizeStyle,
DefaultVerticalAlignStyle,
Geometry2d,
LABEL_FONT_SIZES,
Polygon2d,
ShapeUtil,
T,
TEXT_PROPS,
TLBaseShape,
TLDefaultColorStyle,
TLDefaultSizeStyle,
TLHandle,
TLOnBeforeUpdateHandler,
TLOnHandleDragHandler,
TLOnResizeHandler,
Vec,
VecModel,
ZERO_INDEX_KEY,
deepCopy,
getDefaultColorTheme,
resizeBox,
structuredClone,
useEditorComponents,
vecModelValidator,
} from 'tldraw'
import { getSpeechBubbleVertices, getTailIntersectionPoint } from './helpers'
Expand All @@ -34,42 +38,47 @@ export const STROKE_SIZES = {
// There's a guide at the bottom of this file!

// [1]
export type SpeechBubbleShape = TLBaseShape<
'speech-bubble',
{
w: number
h: number
size: TLDefaultSizeStyle
color: TLDefaultColorStyle
tail: VecModel
}
>

export const speechBubbleShapeProps = {
w: T.number,
h: T.number,
size: DefaultSizeStyle,
color: DefaultColorStyle,
font: DefaultFontStyle,
align: DefaultHorizontalAlignStyle,
verticalAlign: DefaultVerticalAlignStyle,
text: T.string,
tail: vecModelValidator,
}

export type SpeechBubbleShapeProps = ShapePropsType<typeof speechBubbleShapeProps>
export type SpeechBubbleShape = TLBaseShape<'speech-bubble', SpeechBubbleShapeProps>

export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
static override type = 'speech-bubble' as const

// [2]
static override props = {
w: T.number,
h: T.number,
size: DefaultSizeStyle,
color: DefaultColorStyle,
tail: vecModelValidator,
}
static override props = speechBubbleShapeProps

override isAspectRatioLocked = (_shape: SpeechBubbleShape) => false

override canResize = (_shape: SpeechBubbleShape) => true

override canBind = (_shape: SpeechBubbleShape) => true

override canEdit = () => true

// [3]
getDefaultProps(): SpeechBubbleShape['props'] {
getDefaultProps(): SpeechBubbleShapeProps {
return {
w: 200,
h: 130,
color: 'black',
size: 'm',
font: 'draw',
align: 'middle',
verticalAlign: 'start',
text: '',
tail: { x: 0.5, y: 1.5 },
}
}
Expand Down Expand Up @@ -151,6 +160,9 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
}

component(shape: SpeechBubbleShape) {
const {
props: { color, size },
} = shape
const theme = getDefaultColorTheme({
isDarkMode: this.editor.user.getIsDarkMode(),
})
Expand All @@ -162,11 +174,13 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
<svg className="tl-svg-container">
<path
d={pathData}
strokeWidth={STROKE_SIZES[shape.props.size]}
stroke={theme[shape.props.color].solid}
strokeWidth={STROKE_SIZES[size]}
stroke={theme[color].solid}
fill={'none'}
/>
</svg>

<TextLabelWrapper shape={shape} />
</>
)
}
Expand All @@ -188,6 +202,33 @@ export class SpeechBubbleUtil extends ShapeUtil<SpeechBubbleShape> {
}
}

function TextLabelWrapper({ shape }: { shape: SpeechBubbleShape }) {
const { TextLabel } = useEditorComponents()

const {
id,
type,
props: { color, font, align, size, text },
} = shape

return (
TextLabel && (
<TextLabel
id={id}
type={type}
font={font}
fontSize={LABEL_FONT_SIZES[size]}
lineHeight={TEXT_PROPS.lineHeight}
align={align}
verticalAlign="start"
text={text}
labelColor={color}
wrap
/>
)
)
}

/*
Introduction: This file contains our custom shape util. The shape util is a class that defines how
our shape behaves. Most of the logic for how the speech bubble shape works is in the onBeforeUpdate
Expand Down
17 changes: 17 additions & 0 deletions apps/examples/src/shared/TipTap.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* N.B. This is just because our default fonts don't have bolding/italic versions. */

.tiptap strong {
display: inline-block;
filter: drop-shadow(0 0 1px rgba(0, 0, 0, 0.5));
transform: scale(0.9) rotate(-3deg);
opacity: 0.8;
}

.tiptap em {
display: inline-block;
transform: skewX(-12deg);
}

.tiptap p {
margin: 0;
}