add descriptions to examples (#2375)
Adds descriptions to examples. ![Kapture 2023-12-22 at 17 08 32](https://github.com/tldraw/tldraw/assets/1489520/d78657cf-b3c3-4160-b58b-7c08ed27823d) They show as a list on the index page, and on individual examples they show in a three-js style sidebar. For now, this is disabled completely on mobile. Examples can still be opened in 'standalone' mode to get rid of the sidebar. Note: the 'view code' link won't work until after these changes are merged. There's a small impact on authoring examples: each one needs to live in a folder with a README.md. At a minimum, the readme needs to look like this: ```md --- title: My Example component: ./MyExample.tsx --- Here is a 1-liner about my example ``` Optionally, you can: - Add `hide: true` to the frontmatter to remove the example from the list (you can skip the description this way) - Add `order: 3` to control the order in which the example appears. They're alphabetical otherwise - Add some more description or links to docs below a `---`. This won't show in the listing, but will be visible on GitHub and on the example page itself. As a follow-up, I'd like to add an 'Open in CodeSandbox' link to each example. These won't work until we've made a release with these examples (as our special examples codesandbox is tied to our release process) but the code is there & ready to go! Have a play, let me know what you think! ### Change Type - [x] `documentation` — Changes to the documentation only[^2] --------- Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
parent
ed37bcf541
commit
b373abf605
102 changed files with 1772 additions and 355 deletions
|
@ -1,7 +1,7 @@
|
|||
<div alt style="text-align: center; transform: scale(.5);">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/tldraw/tldraw/main/assets/github-hero-dark-2.png" />
|
||||
<img alt="tldraw" src="https://raw.githubusercontent.com/tldraw/tldraw/main/assets/github-hero-light-2.png" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/tldraw/tldraw/main/assets/github-hero-dark-2.png"/>
|
||||
<img alt="tldraw" src="https://raw.githubusercontent.com/tldraw/tldraw/main/assets/github-hero-light-2.png"/>
|
||||
</picture>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@
|
|||
"@tldraw/assets": "workspace:*",
|
||||
"@tldraw/tldraw": "workspace:*",
|
||||
"@vercel/analytics": "^1.0.1",
|
||||
"classnames": "^2.3.2",
|
||||
"lazyrepo": "0.0.0-alpha.27",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
@ -47,6 +48,10 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"dotenv": "^16.0.3"
|
||||
"dotenv": "^16.0.3",
|
||||
"remark": "^15.0.1",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-html": "^16.0.1",
|
||||
"vfile-matter": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
|
79
apps/examples/src/ExamplePage.tsx
Normal file
79
apps/examples/src/ExamplePage.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { assert, assertExists } from '@tldraw/tldraw'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import ExamplesTldrawLogo from './components/ExamplesTldrawLogo'
|
||||
import { ListLink } from './components/ListLink'
|
||||
import { Example, examples } from './examples'
|
||||
|
||||
export function ExamplePage({
|
||||
example,
|
||||
children,
|
||||
}: {
|
||||
example: Example
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const scrollElRef = useRef<HTMLUListElement>(null)
|
||||
const activeElRef = useRef<HTMLLIElement>(null)
|
||||
const isFirstScroll = useRef(true)
|
||||
|
||||
useEffect(() => {
|
||||
const frame = requestAnimationFrame(() => {
|
||||
if (activeElRef.current) {
|
||||
const scrollEl = assertExists(scrollElRef.current)
|
||||
const activeEl = activeElRef.current
|
||||
assert(activeEl.offsetParent === scrollEl)
|
||||
|
||||
const isScrolledIntoView =
|
||||
activeEl.offsetTop >= scrollEl.scrollTop &&
|
||||
activeEl.offsetTop + activeEl.offsetHeight <= scrollEl.scrollTop + scrollEl.offsetHeight
|
||||
|
||||
if (!isScrolledIntoView) {
|
||||
activeEl.scrollIntoView({
|
||||
behavior: isFirstScroll.current ? 'auto' : 'smooth',
|
||||
block: isFirstScroll.current ? 'start' : 'center',
|
||||
})
|
||||
}
|
||||
isFirstScroll.current = false
|
||||
}
|
||||
})
|
||||
return () => cancelAnimationFrame(frame)
|
||||
}, [example])
|
||||
|
||||
return (
|
||||
<div className="example">
|
||||
<div className="example__info">
|
||||
<Link className="example__logo" to="/">
|
||||
<ExamplesTldrawLogo /> examples
|
||||
</Link>
|
||||
<ul className="example__info__list scroll-light" ref={scrollElRef}>
|
||||
{examples
|
||||
.filter((e) => !e.hide)
|
||||
.filter((e) => e.order !== null)
|
||||
.map((e) => (
|
||||
<ListLink
|
||||
key={e.path}
|
||||
ref={e.path === example.path ? activeElRef : undefined}
|
||||
example={e}
|
||||
isActive={e.path === example.path}
|
||||
/>
|
||||
))}
|
||||
<li>
|
||||
<hr />
|
||||
</li>
|
||||
{examples
|
||||
.filter((e) => !e.hide)
|
||||
.filter((e) => e.order === null)
|
||||
.map((e) => (
|
||||
<ListLink
|
||||
key={e.path}
|
||||
ref={e.path === example.path ? activeElRef : undefined}
|
||||
example={e}
|
||||
isActive={e.path === example.path}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="example__content">{children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
35
apps/examples/src/HomePage.tsx
Normal file
35
apps/examples/src/HomePage.tsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import ExamplesTldrawLogo from './components/ExamplesTldrawLogo'
|
||||
import { ListLink } from './components/ListLink'
|
||||
import { examples } from './examples'
|
||||
|
||||
export function HomePage() {
|
||||
return (
|
||||
<div className="examples">
|
||||
<div className="examples__header">
|
||||
<div className="examples__title">
|
||||
<ExamplesTldrawLogo /> examples
|
||||
</div>
|
||||
<p>
|
||||
See docs at <a href="https://tldraw.dev">tldraw.dev</a>
|
||||
</p>
|
||||
</div>
|
||||
<ul className="examples__list">
|
||||
{examples
|
||||
.filter((example) => !example.hide)
|
||||
.filter((example) => example.order !== null)
|
||||
.map((example) => (
|
||||
<ListLink key={example.path} example={example} showDescription />
|
||||
))}
|
||||
</ul>
|
||||
<hr />
|
||||
<ul className="examples__list">
|
||||
{examples
|
||||
.filter((example) => !example.hide)
|
||||
.filter((example) => example.order === null)
|
||||
.map((example) => (
|
||||
<ListLink key={example.path} example={example} showDescription />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
13
apps/examples/src/components/Icons.tsx
Normal file
13
apps/examples/src/components/Icons.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
export function StandaloneIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg width="16" height="16" viewBox="0 0 30 30" fill="none" {...props}>
|
||||
<path
|
||||
d="M13 5H7C5.89543 5 5 5.89543 5 7V23C5 24.1046 5.89543 25 7 25H23C24.1046 25 25 24.1046 25 23V17M19 5H25M25 5V11M25 5L13 17"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
|
@ -1,14 +1,94 @@
|
|||
import classNames from 'classnames'
|
||||
import { ForwardedRef, forwardRef, useEffect, useId, useLayoutEffect, useRef } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { Example } from '../examples'
|
||||
import { useMergedRefs } from '../hooks/useMegedRefs'
|
||||
import { StandaloneIcon } from './Icons'
|
||||
import { Markdown } from './Markdown'
|
||||
|
||||
export interface ListLinkProps {
|
||||
title: string
|
||||
route: string
|
||||
}
|
||||
export const ListLink = forwardRef(function ListLink(
|
||||
{
|
||||
example,
|
||||
isActive,
|
||||
showDescription,
|
||||
}: { example: Example; isActive?: boolean; showDescription?: boolean },
|
||||
ref: ForwardedRef<HTMLLIElement>
|
||||
) {
|
||||
const id = useId()
|
||||
const containerRef = useRef<HTMLLIElement | null>(null)
|
||||
const wasActiveRef = useRef(isActive)
|
||||
useEffect(() => {
|
||||
wasActiveRef.current = isActive
|
||||
}, [isActive])
|
||||
|
||||
const heightBefore =
|
||||
wasActiveRef.current !== isActive ? containerRef.current?.offsetHeight : undefined
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (heightBefore !== undefined && containerRef.current) {
|
||||
containerRef.current.animate(
|
||||
[{ height: heightBefore + 'px' }, { height: containerRef.current.offsetHeight + 'px' }],
|
||||
{
|
||||
duration: 120,
|
||||
easing: 'ease-out',
|
||||
fill: 'backwards',
|
||||
delay: 100,
|
||||
}
|
||||
)
|
||||
}
|
||||
}, [heightBefore])
|
||||
|
||||
const mainDetails = (
|
||||
<>
|
||||
<h3 id={id}>
|
||||
{example.title}
|
||||
{isActive && (
|
||||
<Link
|
||||
to={`${example.path}/full`}
|
||||
aria-label="Standalone"
|
||||
className="examples__list__item__standalone"
|
||||
title="View standalone example"
|
||||
>
|
||||
<StandaloneIcon />
|
||||
</Link>
|
||||
)}
|
||||
</h3>
|
||||
{showDescription && <Markdown sanitizedHtml={example.description} />}
|
||||
</>
|
||||
)
|
||||
|
||||
// TODO: re-enable code sandbox links
|
||||
// const codeSandboxPath = encodeURIComponent(
|
||||
// `/src/examples${example.path}${example.componentFile.replace(/^\./, '')}`
|
||||
// )
|
||||
const extraDetails = (
|
||||
<div className="examples__list__item__details" aria-hidden={!isActive}>
|
||||
<Markdown sanitizedHtml={example.details} />
|
||||
<div className="examples__list__item__code">
|
||||
<a href={example.codeUrl} target="_blank" rel="noreferrer">
|
||||
View code
|
||||
</a>
|
||||
{/* <a
|
||||
href={`https://codesandbox.io/p/devbox/github/tldraw/tldraw/tree/examples/?file=${codeSandboxPath}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Edit in CodeSandbox
|
||||
</a> */}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
export function ListLink({ title, route }: ListLinkProps) {
|
||||
return (
|
||||
<li className="examples__list__item">
|
||||
<Link to={route}>{title}</Link>
|
||||
<li
|
||||
ref={useMergedRefs(ref, containerRef)}
|
||||
className={classNames('examples__list__item', isActive && 'examples__list__item__active')}
|
||||
>
|
||||
{!isActive && (
|
||||
<Link to={example.path} aria-labelledby={id} className="examples__list__item__link" />
|
||||
)}
|
||||
{mainDetails}
|
||||
{extraDetails}
|
||||
</li>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
|
16
apps/examples/src/components/Markdown.tsx
Normal file
16
apps/examples/src/components/Markdown.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import classNames from 'classnames'
|
||||
|
||||
export function Markdown({
|
||||
sanitizedHtml,
|
||||
className = '',
|
||||
}: {
|
||||
sanitizedHtml: string
|
||||
className?: string
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={classNames('examples__markdown', className)}
|
||||
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
|
||||
/>
|
||||
)
|
||||
}
|
19
apps/examples/src/components/Spinner.tsx
Normal file
19
apps/examples/src/components/Spinner.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
export function Spinner(props: React.SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg width={16} height={16} viewBox="0 0 16 16" {...props}>
|
||||
<g strokeWidth={2} fill="none" fillRule="evenodd">
|
||||
<circle strokeOpacity={0.25} cx={8} cy={8} r={7} stroke="currentColor" />
|
||||
<path strokeLinecap="round" d="M15 8c0-4.5-4.5-7-7-7" stroke="currentColor">
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
type="rotate"
|
||||
from="0 8 8"
|
||||
to="360 8 8"
|
||||
dur="1s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</path>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
28
apps/examples/src/examples.tsx
Normal file
28
apps/examples/src/examples.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { ComponentType } from 'react'
|
||||
|
||||
export type Example = {
|
||||
title: string
|
||||
description: string
|
||||
details: string
|
||||
path: string
|
||||
codeUrl: string
|
||||
hide: boolean
|
||||
order: number | null
|
||||
componentFile: string
|
||||
loadComponent: () => Promise<ComponentType>
|
||||
}
|
||||
|
||||
export const examples = (
|
||||
Object.values(import.meta.glob('./examples/*/README.md', { eager: true })) as Example[]
|
||||
).sort((a, b) => {
|
||||
// sort by order then title:
|
||||
if (a.order === b.order) {
|
||||
return a.title.localeCompare(b.title)
|
||||
} else if (a.order === null) {
|
||||
return 1
|
||||
} else if (b.order === null) {
|
||||
return -1
|
||||
} else {
|
||||
return a.order - b.order
|
||||
}
|
||||
})
|
11
apps/examples/src/examples/api/README.md
Normal file
11
apps/examples/src/examples/api/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: Editor API
|
||||
component: ./APIExample.tsx
|
||||
order: 2
|
||||
---
|
||||
|
||||
Manipulate the contents of the canvas using the editor API.
|
||||
|
||||
---
|
||||
|
||||
This example creates and updates shapes, selects and rotates them, and zooms the camera.
|
10
apps/examples/src/examples/asset-props/README.md
Normal file
10
apps/examples/src/examples/asset-props/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: Asset props
|
||||
component: ./AssetPropsExample.tsx
|
||||
---
|
||||
|
||||
Control the assets (images, videos, etc.) that can be added to the canvas.
|
||||
|
||||
---
|
||||
|
||||
This example demonstrates the `<Tldraw/>` component's props that give you control over assets: which types are allowed, the maximum size, and maximum dimensions.
|
|
@ -11,34 +11,32 @@ export default function CanvasEventsExample() {
|
|||
|
||||
return (
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div style={{ width: '50vw', height: '100vh' }}>
|
||||
<div style={{ width: '50%', height: '100vh' }}>
|
||||
<Tldraw
|
||||
onMount={(editor) => {
|
||||
editor.on('event', (event) => handleEvent(event))
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
width: '50vw',
|
||||
height: '100vh',
|
||||
padding: 8,
|
||||
background: '#eee',
|
||||
border: 'none',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
borderLeft: 'solid 2px #333',
|
||||
display: 'flex',
|
||||
flexDirection: 'column-reverse',
|
||||
overflow: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
>
|
||||
{events.map((t, i) => (
|
||||
<div key={i}>{t}</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: '50%',
|
||||
height: '100vh',
|
||||
padding: 8,
|
||||
background: '#eee',
|
||||
border: 'none',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
borderLeft: 'solid 2px #333',
|
||||
display: 'flex',
|
||||
flexDirection: 'column-reverse',
|
||||
overflow: 'auto',
|
||||
whiteSpace: 'pre-wrap',
|
||||
}}
|
||||
>
|
||||
{events.map((t, i) => (
|
||||
<div key={i}>{t}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
10
apps/examples/src/examples/canvas-events/README.md
Normal file
10
apps/examples/src/examples/canvas-events/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: Canvas events
|
||||
component: ./CanvasEventsExample.tsx
|
||||
---
|
||||
|
||||
Listen to events from tldraw's canvas.
|
||||
|
||||
---
|
||||
|
||||
These are the input events that the editor interprets. Try moving your cursor, dragging, using modifier keys, etc.
|
12
apps/examples/src/examples/custom-components/README.md
Normal file
12
apps/examples/src/examples/custom-components/README.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
title: Canvas components
|
||||
component: ./CustomComponentsExample.tsx
|
||||
---
|
||||
|
||||
Replace tldraw's on-canvas UI with your own.
|
||||
|
||||
---
|
||||
|
||||
Tldraw's on-canvas UI is build from replaceable React components.
|
||||
|
||||
Try dragging to select or using the eraser tool to see the custom components in this example.
|
11
apps/examples/src/examples/custom-config/README.md
Normal file
11
apps/examples/src/examples/custom-config/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: Custom shapes / tools
|
||||
component: ./CustomConfigExample.tsx
|
||||
order: 3
|
||||
---
|
||||
|
||||
Create custom shapes / tools
|
||||
|
||||
---
|
||||
|
||||
The card shape (select ⚫️ in the toolbar) is a custom shape - but also just a normal react component.
|
10
apps/examples/src/examples/custom-styles/README.md
Normal file
10
apps/examples/src/examples/custom-styles/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: Custom styles
|
||||
component: ./CustomStylesExample.tsx
|
||||
---
|
||||
|
||||
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.
|
10
apps/examples/src/examples/custom-ui/README.md
Normal file
10
apps/examples/src/examples/custom-ui/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: Custom UI
|
||||
component: ./CustomUiExample.tsx
|
||||
---
|
||||
|
||||
Replace tldraw's UI with your own.
|
||||
|
||||
---
|
||||
|
||||
This UI has keyboard shortcuts and buttons for selecting tools.
|
11
apps/examples/src/examples/develop/README.md
Normal file
11
apps/examples/src/examples/develop/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: Basic (development)
|
||||
component: ./BasicExample.tsx
|
||||
order: 1
|
||||
---
|
||||
|
||||
The easiest way to get started with tldraw.
|
||||
|
||||
---
|
||||
|
||||
The simplest use of the `<Tldraw/>` component.
|
10
apps/examples/src/examples/error-boundary/README.md
Normal file
10
apps/examples/src/examples/error-boundary/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: Error boundary
|
||||
component: ./ErrorBoundaryExample.tsx
|
||||
---
|
||||
|
||||
Catch errors in shapes.
|
||||
|
||||
---
|
||||
|
||||
When something goes wrong in a shape, it won't crash the whole editor - just the shape that went wrong.
|
10
apps/examples/src/examples/exploded/README.md
Normal file
10
apps/examples/src/examples/exploded/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: Sublibraries
|
||||
component: ./ExplodedExample.tsx
|
||||
---
|
||||
|
||||
Tldraw is built from several sublibraries - like the editor, default shapes & tools, and UI.
|
||||
|
||||
---
|
||||
|
||||
For full customization, you can use these sublibraries directly, or replace them with your own.
|
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: External content sources
|
||||
component: ./ExternalContentSourcesExample.tsx
|
||||
---
|
||||
|
||||
Control what happens when the user pastes content into the editor.
|
||||
|
||||
---
|
||||
|
||||
In this example, we register a special handler for when the user pastes in 'text/html' content. We add it to a special shape type that renders the html content directly.
|
5
apps/examples/src/examples/floaty-window/README.md
Normal file
5
apps/examples/src/examples/floaty-window/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Floaty window
|
||||
hide: true
|
||||
component: ./FloatyExample.tsx
|
||||
---
|
6
apps/examples/src/examples/force-mobile/README.md
Normal file
6
apps/examples/src/examples/force-mobile/README.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Force mobile breakpoint
|
||||
component: ./ForceBreakpointExample
|
||||
---
|
||||
|
||||
Force the editor UI to render as if it were on a mobile device.
|
10
apps/examples/src/examples/hide-ui/README.md
Normal file
10
apps/examples/src/examples/hide-ui/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: Hide UI
|
||||
component: ./HideUiExample.tsx
|
||||
---
|
||||
|
||||
Hide tldraw's UI with the `hideUi` prop.
|
||||
|
||||
---
|
||||
|
||||
Useful for a bare-bones editor, or if you want to build your own UI.
|
5
apps/examples/src/examples/hosted-images/README.md
Normal file
5
apps/examples/src/examples/hosted-images/README.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Hosted images
|
||||
component: ./HostedImagesExample.tsx
|
||||
hide: true
|
||||
---
|
|
@ -5,7 +5,7 @@ import { useCallback } from 'react'
|
|||
// This is an example of how you can add an image to the editor. The image is already
|
||||
// present in the `public` folder, so we can just use it directly.
|
||||
// If you want to allow users to upload the images please take a look at the `HostedImagesExample.tsx`
|
||||
export default function ImageExample() {
|
||||
export default function LocalImagesExample() {
|
||||
const handleMount = useCallback((editor: Editor) => {
|
||||
// Assets are records that store data about shared assets like images, videos, etc.
|
||||
// Each image has an associated asset record, so we'll create that first.
|
||||
|
@ -22,7 +22,7 @@ export default function ImageExample() {
|
|||
typeName: 'asset',
|
||||
props: {
|
||||
name: 'tldraw.png',
|
||||
src: '/tldraw.png',
|
||||
src: '/tldraw.png', // You could also use a base64 encoded string here
|
||||
w: imageWidth,
|
||||
h: imageHeight,
|
||||
mimeType: 'image/png',
|
||||
|
@ -51,7 +51,10 @@ export default function ImageExample() {
|
|||
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw persistenceKey="tldraw_example" onMount={handleMount} />
|
||||
<Tldraw
|
||||
// persistenceKey="tldraw_local_images_example"
|
||||
onMount={handleMount}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
11
apps/examples/src/examples/local-images/README.md
Normal file
11
apps/examples/src/examples/local-images/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: Local images
|
||||
component: ./LocalImagesExample.tsx
|
||||
hide: false
|
||||
---
|
||||
|
||||
How to use local images in the built-in `ImageShape` shape.
|
||||
|
||||
---
|
||||
|
||||
This example shows how to use local images in the built-in `ImageShape` shape. You must first create an asset that holds the source of the image, then create a shape that references the asset.
|
|
@ -1,19 +1,16 @@
|
|||
import { Tldraw } from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
|
||||
export default function MetaExample() {
|
||||
export default function OnChangeShapeMetaExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw
|
||||
persistenceKey="tldraw_example"
|
||||
onMount={(editor) => {
|
||||
// There's no API for setting getInitialMetaForShape yet, but
|
||||
// you can replace it at runtime like this. This will run for
|
||||
// all shapes created by the user.
|
||||
// See the "meta-on-create" example for more about setting the
|
||||
// initial meta for a shape.
|
||||
editor.getInitialMetaForShape = (_shape) => {
|
||||
return {
|
||||
createdBy: editor.user.getId(),
|
||||
createdAt: Date.now(),
|
||||
updatedBy: editor.user.getId(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
|
@ -21,14 +18,13 @@ export default function MetaExample() {
|
|||
// We can also use the sideEffects API to modify a shape before
|
||||
// its change is committed to the database. This will run for
|
||||
// all shapes whenever they are updated.
|
||||
editor.sideEffects.registerBeforeChangeHandler('shape', (record, _prev, source) => {
|
||||
if (source !== 'user') return record
|
||||
record.meta = {
|
||||
...record.meta,
|
||||
editor.sideEffects.registerBeforeChangeHandler('shape', (_prev, next, source) => {
|
||||
if (source !== 'user') return next
|
||||
next.meta = {
|
||||
updatedBy: editor.user.getId(),
|
||||
updatedAt: Date.now(),
|
||||
}
|
||||
return record
|
||||
return next
|
||||
})
|
||||
}}
|
||||
/>
|
10
apps/examples/src/examples/meta-on-change/README.md
Normal file
10
apps/examples/src/examples/meta-on-change/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: Shape Meta (on change)
|
||||
component: ./OnChangeShapeMetaExample.tsx
|
||||
---
|
||||
|
||||
Add custom metadata to shapes when they're changed.
|
||||
|
||||
---
|
||||
|
||||
In this example, we add updatedAt metadata to shapes when they're updated.
|
|
@ -0,0 +1,23 @@
|
|||
import { Tldraw } from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
|
||||
export default function OnCreateShapeMetaExample() {
|
||||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw
|
||||
persistenceKey="tldraw_example"
|
||||
onMount={(editor) => {
|
||||
// There's no API for setting getInitialMetaForShape yet, but
|
||||
// you can replace it at runtime like this. This will run for
|
||||
// all shapes created by the user.
|
||||
editor.getInitialMetaForShape = (_shape) => {
|
||||
return {
|
||||
createdBy: editor.user.getId(),
|
||||
createdAt: Date.now(),
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
10
apps/examples/src/examples/meta-on-create/README.md
Normal file
10
apps/examples/src/examples/meta-on-create/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: Shape Meta (on create)
|
||||
component: ./OnCreateShapeMetaExample.tsx
|
||||
---
|
||||
|
||||
Add custom metadata to shapes when they're created.
|
||||
|
||||
---
|
||||
|
||||
In this example, we add createdBy and createdAt metadata to shapes when they're created.
|
6
apps/examples/src/examples/multiple/README.md
Normal file
6
apps/examples/src/examples/multiple/README.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Multiple editors
|
||||
component: ./MultipleExample.tsx
|
||||
---
|
||||
|
||||
Use multiple <Tldraw/> components on the same page.
|
10
apps/examples/src/examples/only-editor/README.md
Normal file
10
apps/examples/src/examples/only-editor/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: Minimal
|
||||
component: ./OnlyEditor.tsx
|
||||
---
|
||||
|
||||
Use the `<TldrawEditor/>` component to render a bare-bones editor with minimal built-in shapes and tools.
|
||||
|
||||
---
|
||||
|
||||
This example show show the `<TldrawEditor/>` component can be used to render a bare-bones editor. It uses minimal built-in shapes and tools.
|
11
apps/examples/src/examples/persistence/README.md
Normal file
11
apps/examples/src/examples/persistence/README.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: Persistence
|
||||
component: ./PersistenceExample.tsx
|
||||
order: 5
|
||||
---
|
||||
|
||||
Save the contents of the editor
|
||||
|
||||
---
|
||||
|
||||
In this example, we load the contents of the editor from your browser's localStorage, and save it there when you make changes.
|
10
apps/examples/src/examples/readonly/README.md
Normal file
10
apps/examples/src/examples/readonly/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: Readonly
|
||||
component: ./ReadOnlyExample
|
||||
---
|
||||
|
||||
Use the editor in readonly mode.
|
||||
|
||||
---
|
||||
|
||||
Users can still pan and zoom the canvas, but they can't change the contents.
|
10
apps/examples/src/examples/screenshot-tool/README.md
Normal file
10
apps/examples/src/examples/screenshot-tool/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: Custom tool (screenshot)
|
||||
component: ./ScreenshotToolExample.tsx
|
||||
---
|
||||
|
||||
Draw a box on the canvas to capture a screenshot of that area.
|
||||
|
||||
---
|
||||
|
||||
Tools are the parts of tldraw's state chart. Most interactions in tldraw are tools.
|
|
@ -4,6 +4,7 @@ import {
|
|||
TLUiAssetUrlOverrides,
|
||||
TLUiOverrides,
|
||||
Tldraw,
|
||||
Vec2d,
|
||||
toolbarItem,
|
||||
useEditor,
|
||||
useValue,
|
||||
|
@ -68,7 +69,10 @@ function ScreenshotBox() {
|
|||
// "page space", i.e. uneffected by scale, and relative to the tldraw
|
||||
// page's top left corner.
|
||||
const zoomLevel = editor.getZoomLevel()
|
||||
const { x, y } = editor.pageToScreen({ x: box.x, y: box.y })
|
||||
const { x, y } = Vec2d.Sub(
|
||||
editor.pageToScreen({ x: box.x, y: box.y }),
|
||||
editor.getViewportScreenBounds()
|
||||
)
|
||||
return new Box2d(x, y, box.w * zoomLevel, box.h * zoomLevel)
|
||||
},
|
||||
[editor]
|
10
apps/examples/src/examples/scroll/README.md
Normal file
10
apps/examples/src/examples/scroll/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: Scrolling container
|
||||
component: ./ScrollExample.tsx
|
||||
---
|
||||
|
||||
Use the editor inside a scrollable container.
|
||||
|
||||
---
|
||||
|
||||
Tldraw doesn't have to be full screen.
|
10
apps/examples/src/examples/shape-meta/README.md
Normal file
10
apps/examples/src/examples/shape-meta/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: Shape meta
|
||||
component: ./ShapeMetaExample.tsx
|
||||
---
|
||||
|
||||
Add a label to shapes with the meta property.
|
||||
|
||||
---
|
||||
|
||||
Select a shape and try changing its label. The label is stored in the shape's meta property, which can be used to add custom data to any shape.
|
10
apps/examples/src/examples/snapshots/README.md
Normal file
10
apps/examples/src/examples/snapshots/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: Snapshots
|
||||
component: ./SnapshotExample.tsx
|
||||
---
|
||||
|
||||
Load a snapshot of the editor's contents.
|
||||
|
||||
---
|
||||
|
||||
Use `editor.store.getSnapshot()` and `editor.store.loadSnapshot()` to save and restore the editor's contents.
|
|
@ -10,7 +10,7 @@ const tools = [SpeechBubbleTool]
|
|||
|
||||
export default function CustomShapeWithHandles() {
|
||||
return (
|
||||
<div style={{ position: 'fixed', inset: 0 }}>
|
||||
<div style={{ position: 'absolute', inset: 0 }}>
|
||||
<Tldraw
|
||||
shapeUtils={shapeUtils}
|
||||
tools={tools}
|
6
apps/examples/src/examples/speech-bubble/README.md
Normal file
6
apps/examples/src/examples/speech-bubble/README.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Speech bubble
|
||||
component: ./CustomShapeWithHandles.tsx
|
||||
---
|
||||
|
||||
A custom shape with handles
|
10
apps/examples/src/examples/store-events/README.md
Normal file
10
apps/examples/src/examples/store-events/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: Store events
|
||||
component: ./StoreEventsExample.tsx
|
||||
---
|
||||
|
||||
Listen to changes from tldraw's store.
|
||||
|
||||
---
|
||||
|
||||
Try creating & deleting shapes, or switching pages. The changes will be logged next to the canvas.
|
|
@ -57,29 +57,27 @@ export default function StoreEventsExample() {
|
|||
|
||||
return (
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div style={{ width: '60vw', height: '100vh' }}>
|
||||
<div style={{ width: '60%', height: '100vh' }}>
|
||||
<Tldraw onMount={setAppToState} />
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
width: '40vw',
|
||||
height: '100vh',
|
||||
padding: 8,
|
||||
background: '#eee',
|
||||
border: 'none',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
borderLeft: 'solid 2px #333',
|
||||
display: 'flex',
|
||||
flexDirection: 'column-reverse',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{storeEvents.map((t, i) => (
|
||||
<div key={i}>{t}</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: '40%',
|
||||
height: '100vh',
|
||||
padding: 8,
|
||||
background: '#eee',
|
||||
border: 'none',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
borderLeft: 'solid 2px #333',
|
||||
display: 'flex',
|
||||
flexDirection: 'column-reverse',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{storeEvents.map((t, i) => (
|
||||
<div key={i}>{t}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
|
@ -1,4 +1,11 @@
|
|||
import { stopEventPropagation, Tldraw, TLEditorComponents, track, useEditor } from '@tldraw/tldraw'
|
||||
import {
|
||||
stopEventPropagation,
|
||||
Tldraw,
|
||||
TLEditorComponents,
|
||||
track,
|
||||
useEditor,
|
||||
Vec2d,
|
||||
} from '@tldraw/tldraw'
|
||||
import '@tldraw/tldraw/tldraw.css'
|
||||
import { useState } from 'react'
|
||||
|
||||
|
@ -55,7 +62,10 @@ const MyComponentInFront = track(() => {
|
|||
|
||||
if (!selectionRotatedPageBounds) return null
|
||||
|
||||
const pageCoordinates = editor.pageToScreen(selectionRotatedPageBounds.point)
|
||||
const pageCoordinates = Vec2d.Sub(
|
||||
editor.pageToScreen(selectionRotatedPageBounds.point),
|
||||
editor.getViewportScreenBounds()
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
10
apps/examples/src/examples/things-on-the-canvas/README.md
Normal file
10
apps/examples/src/examples/things-on-the-canvas/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: Things on the canvas
|
||||
component: ./OnTheCanvas.tsx
|
||||
---
|
||||
|
||||
Add custom components to the editor
|
||||
|
||||
---
|
||||
|
||||
Components can either float on top of the canvas unaffected by the camera, or be a part of the canvas itself.
|
10
apps/examples/src/examples/ui-events/README.md
Normal file
10
apps/examples/src/examples/ui-events/README.md
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
title: UI events
|
||||
component: ./UiEventsExample.tsx
|
||||
---
|
||||
|
||||
Listen to events from tldraw's UI.
|
||||
|
||||
---
|
||||
|
||||
Try selecting tools, using keyboard shortcuts, undo/redo, etc. Events will be logged next to the canvas.
|
|
@ -11,29 +11,27 @@ export default function UiEventsExample() {
|
|||
|
||||
return (
|
||||
<div style={{ display: 'flex' }}>
|
||||
<div style={{ width: '60vw', height: '100vh' }}>
|
||||
<div style={{ width: '60%', height: '100vh' }}>
|
||||
<Tldraw onUiEvent={handleUiEvent} />
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
width: '40vw',
|
||||
height: '100vh',
|
||||
padding: 8,
|
||||
background: '#eee',
|
||||
border: 'none',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
borderLeft: 'solid 2px #333',
|
||||
display: 'flex',
|
||||
flexDirection: 'column-reverse',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{uiEvents.map((t, i) => (
|
||||
<div key={i}>{t}</div>
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
width: '40%',
|
||||
height: '100vh',
|
||||
padding: 8,
|
||||
background: '#eee',
|
||||
border: 'none',
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
borderLeft: 'solid 2px #333',
|
||||
display: 'flex',
|
||||
flexDirection: 'column-reverse',
|
||||
overflow: 'auto',
|
||||
}}
|
||||
>
|
||||
{uiEvents.map((t, i) => (
|
||||
<div key={i}>{t}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
12
apps/examples/src/examples/user-presence/README.md
Normal file
12
apps/examples/src/examples/user-presence/README.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
title: User presence
|
||||
component: ./UserPresenceExample.tsx
|
||||
---
|
||||
|
||||
Show other users editing the same document.
|
||||
|
||||
---
|
||||
|
||||
Here, we add fake InstancePresence records to the store to simulate other users.
|
||||
|
||||
If you have your own presence system, you could add real records to the store in the same way.
|
12
apps/examples/src/examples/zones/README.md
Normal file
12
apps/examples/src/examples/zones/README.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
title: UI zones
|
||||
component: ./ZonesExample.tsx
|
||||
---
|
||||
|
||||
Inject custom components into tldraw's UI.
|
||||
|
||||
---
|
||||
|
||||
Our default UI has two empty "zones" - the `topZone` (in the top-center of the screen) and `shareZone` (in the top right).
|
||||
|
||||
You can set these zones to any React component you want.
|
16
apps/examples/src/hooks/useMegedRefs.tsx
Normal file
16
apps/examples/src/hooks/useMegedRefs.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { ForwardedRef, useCallback } from 'react'
|
||||
|
||||
export function useMergedRefs<T>(...refs: ForwardedRef<T>[]) {
|
||||
return useCallback(
|
||||
(node: T) => {
|
||||
for (const ref of refs) {
|
||||
if (typeof ref === 'function') {
|
||||
ref(node)
|
||||
} else if (ref != null) {
|
||||
ref.current = node
|
||||
}
|
||||
}
|
||||
},
|
||||
[refs]
|
||||
)
|
||||
}
|
|
@ -5,46 +5,12 @@ import {
|
|||
setDefaultEditorAssetUrls,
|
||||
setDefaultUiAssetUrls,
|
||||
} from '@tldraw/tldraw'
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { RouterProvider, createBrowserRouter } from 'react-router-dom'
|
||||
|
||||
import ExamplesTldrawLogo from './components/ExamplesTldrawLogo'
|
||||
import { ListLink } from './components/ListLink'
|
||||
|
||||
import BasicExample from './BasicExample'
|
||||
import APIExample from './examples/APIExample'
|
||||
import AssetPropsExample from './examples/AssetOptionsExample'
|
||||
import CanvasEventsExample from './examples/CanvasEventsExample'
|
||||
import CustomComponentsExample from './examples/CustomComponentsExample'
|
||||
import CustomConfigExample from './examples/CustomConfigExample/CustomConfigExample'
|
||||
import CustomShapeWithHandles from './examples/CustomShapeWithHandles/CustomShapeWithHandles'
|
||||
import CustomStylesExample from './examples/CustomStylesExample/CustomStylesExample'
|
||||
import CustomUiExample from './examples/CustomUiExample/CustomUiExample'
|
||||
import ErrorBoundaryExample from './examples/ErrorBoundaryExample/ErrorBoundaryExample'
|
||||
import ExplodedExample from './examples/ExplodedExample'
|
||||
import ExternalContentSourcesExample from './examples/ExternalContentSourcesExample'
|
||||
import FloatyExample from './examples/FloatyExample'
|
||||
import ForceMobileExample from './examples/ForceBreakpointExample'
|
||||
import HideUiExample from './examples/HideUiExample'
|
||||
import HostedImagesExample from './examples/HostedImagesExample'
|
||||
import ImageExample from './examples/ImagesExamples'
|
||||
import MetaExample from './examples/MetaExample'
|
||||
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'
|
||||
import StoreEventsExample from './examples/StoreEventsExample'
|
||||
import UiEventsExample from './examples/UiEventsExample'
|
||||
import UserPresenceExample from './examples/UserPresenceExample'
|
||||
import ZonesExample from './examples/ZonesExample'
|
||||
import EndToEnd from './examples/end-to-end/end-to-end'
|
||||
import OnlyEditorExample from './examples/only-editor/OnlyEditor'
|
||||
import YjsExample from './examples/yjs/YjsExample'
|
||||
import { ExamplePage } from './ExamplePage'
|
||||
import { HomePage } from './HomePage'
|
||||
import { examples } from './examples'
|
||||
import EndToEnd from './testing/end-to-end'
|
||||
|
||||
// This example is only used for end to end tests
|
||||
|
||||
|
@ -54,219 +20,50 @@ const assetUrls = getAssetUrlsByMetaUrl()
|
|||
setDefaultEditorAssetUrls(assetUrls)
|
||||
setDefaultUiAssetUrls(assetUrls)
|
||||
|
||||
type Example = {
|
||||
path: string
|
||||
title?: string
|
||||
element: JSX.Element
|
||||
}
|
||||
|
||||
export const allExamples: Example[] = [
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
title: 'Basic (development)',
|
||||
path: 'develop',
|
||||
element: <BasicExample />,
|
||||
path: '/',
|
||||
element: <HomePage />,
|
||||
},
|
||||
{
|
||||
title: 'Collaboration (with Yjs)',
|
||||
path: 'yjs',
|
||||
element: <YjsExample />,
|
||||
},
|
||||
{
|
||||
title: 'Editor API',
|
||||
path: 'api',
|
||||
element: <APIExample />,
|
||||
},
|
||||
{
|
||||
title: 'Multiple editors',
|
||||
path: 'multiple',
|
||||
element: <MultipleExample />,
|
||||
},
|
||||
{
|
||||
title: 'Meta Example',
|
||||
path: 'meta',
|
||||
element: <MetaExample />,
|
||||
},
|
||||
{
|
||||
title: 'Readonly Example',
|
||||
path: 'readonly',
|
||||
element: <ReadOnlyExample />,
|
||||
},
|
||||
{
|
||||
title: 'Things on the canvas',
|
||||
path: 'things-on-the-canvas',
|
||||
element: <OnTheCanvasExample />,
|
||||
},
|
||||
{
|
||||
title: 'Scroll example',
|
||||
path: 'scroll',
|
||||
element: <ScrollExample />,
|
||||
},
|
||||
{
|
||||
title: 'Custom shapes / tools',
|
||||
path: 'custom-config',
|
||||
element: <CustomConfigExample />,
|
||||
},
|
||||
{
|
||||
title: 'Sublibraries',
|
||||
path: 'exploded',
|
||||
element: <ExplodedExample />,
|
||||
},
|
||||
{
|
||||
title: 'Error boundary',
|
||||
path: 'error-boundary',
|
||||
element: <ErrorBoundaryExample />,
|
||||
},
|
||||
{
|
||||
title: 'Custom UI',
|
||||
path: 'custom-ui',
|
||||
element: <CustomUiExample />,
|
||||
},
|
||||
{
|
||||
title: 'Custom Tool (Screenshot)',
|
||||
path: 'screenshot-tool',
|
||||
element: <ScreenshotToolExample />,
|
||||
},
|
||||
{
|
||||
title: 'Hide UI',
|
||||
path: 'hide-ui',
|
||||
element: <HideUiExample />,
|
||||
},
|
||||
{
|
||||
title: 'UI components',
|
||||
path: 'custom-components',
|
||||
element: <CustomComponentsExample />,
|
||||
},
|
||||
{
|
||||
title: 'UI events',
|
||||
path: 'ui-events',
|
||||
element: <UiEventsExample />,
|
||||
},
|
||||
{
|
||||
title: 'Canvas events',
|
||||
path: 'canvas-events',
|
||||
element: <CanvasEventsExample />,
|
||||
},
|
||||
{
|
||||
title: 'Store events',
|
||||
path: 'store-events',
|
||||
element: <StoreEventsExample />,
|
||||
},
|
||||
{
|
||||
title: 'User presence',
|
||||
path: 'user-presence',
|
||||
element: <UserPresenceExample />,
|
||||
},
|
||||
{
|
||||
title: 'UI zones',
|
||||
path: 'zones',
|
||||
element: <ZonesExample />,
|
||||
},
|
||||
{
|
||||
title: 'Persistence',
|
||||
path: 'persistence',
|
||||
element: <PersistenceExample />,
|
||||
},
|
||||
{
|
||||
title: 'Snapshots',
|
||||
path: 'snapshots',
|
||||
element: <SnapshotExample />,
|
||||
},
|
||||
{
|
||||
title: 'Force mobile breakpoint',
|
||||
path: 'force-mobile',
|
||||
element: <ForceMobileExample />,
|
||||
},
|
||||
{
|
||||
title: 'Custom styles',
|
||||
path: 'custom-styles',
|
||||
element: <CustomStylesExample />,
|
||||
},
|
||||
{
|
||||
title: 'Shape meta property',
|
||||
path: 'shape-meta',
|
||||
element: <ShapeMetaExample />,
|
||||
},
|
||||
{
|
||||
title: 'Only editor',
|
||||
path: 'only-editor',
|
||||
element: <OnlyEditorExample />,
|
||||
},
|
||||
{
|
||||
title: 'Adding images',
|
||||
path: 'images',
|
||||
element: <ImageExample />,
|
||||
},
|
||||
{
|
||||
title: 'Hosted images example',
|
||||
path: 'hosted-images',
|
||||
element: <HostedImagesExample />,
|
||||
},
|
||||
{
|
||||
title: 'Asset props',
|
||||
path: 'asset-props',
|
||||
element: <AssetPropsExample />,
|
||||
},
|
||||
{
|
||||
title: 'Floaty window',
|
||||
path: 'floaty-window',
|
||||
element: <FloatyExample />,
|
||||
},
|
||||
{
|
||||
title: 'External content sources',
|
||||
path: 'external-content-sources',
|
||||
element: <ExternalContentSourcesExample />,
|
||||
},
|
||||
{
|
||||
title: 'Custom Shape With Handles',
|
||||
path: 'custom-shape-with-handles',
|
||||
element: <CustomShapeWithHandles />,
|
||||
},
|
||||
// not listed
|
||||
{
|
||||
path: 'end-to-end',
|
||||
element: <EndToEnd />,
|
||||
},
|
||||
]
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="examples">
|
||||
<div className="examples__header">
|
||||
<ExamplesTldrawLogo />
|
||||
<p>
|
||||
See docs at <a href="https://tldraw.dev">tldraw.dev</a>
|
||||
</p>
|
||||
</div>
|
||||
<ul className="examples__list">
|
||||
{allExamples
|
||||
.filter((example) => example.title)
|
||||
.map((example) => (
|
||||
<ListLink key={example.path} title={example.title!} route={example.path} />
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <App />,
|
||||
},
|
||||
...allExamples,
|
||||
...examples.flatMap((example) => [
|
||||
{
|
||||
path: example.path,
|
||||
lazy: async () => {
|
||||
const Component = await example.loadComponent()
|
||||
return {
|
||||
element: (
|
||||
<ExamplePage example={example}>
|
||||
<Component />
|
||||
</ExamplePage>
|
||||
),
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
path: `${example.path}/full`,
|
||||
lazy: async () => {
|
||||
const Component = await example.loadComponent()
|
||||
return {
|
||||
element: <Component />,
|
||||
}
|
||||
},
|
||||
},
|
||||
]),
|
||||
])
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const rootElement = document.getElementById('root')!
|
||||
const root = createRoot(rootElement!)
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<ErrorBoundary
|
||||
fallback={(error) => <DefaultErrorFallback error={error} />}
|
||||
onError={(error) => console.error(error)}
|
||||
>
|
||||
<RouterProvider router={router} />
|
||||
</ErrorBoundary>
|
||||
</StrictMode>
|
||||
<ErrorBoundary
|
||||
fallback={(error) => <DefaultErrorFallback error={error} />}
|
||||
onError={(error) => console.error(error)}
|
||||
>
|
||||
<RouterProvider router={router} />
|
||||
</ErrorBoundary>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap');
|
||||
|
||||
html,
|
||||
body {
|
||||
|
@ -13,6 +13,12 @@ body {
|
|||
min-height: -webkit-fill-available;
|
||||
height: 100%;
|
||||
}
|
||||
@media screen and (max-width: 600px) {
|
||||
html,
|
||||
body {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
* {
|
||||
|
@ -20,45 +26,258 @@ html,
|
|||
}
|
||||
|
||||
.tldraw__editor {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
inset: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.examples {
|
||||
padding: 16px;
|
||||
padding: 1.5rem;
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.examples hr {
|
||||
border: none;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.examples__header {
|
||||
width: fit-content;
|
||||
padding-bottom: 32px;
|
||||
padding-bottom: 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
@media screen and (max-width: 500px) {
|
||||
.examples__header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
.examples__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: auto;
|
||||
font-size: 1.45rem;
|
||||
}
|
||||
|
||||
.examples__lockup {
|
||||
height: 56px;
|
||||
height: 1.875rem;
|
||||
width: auto;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.examples__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin: 0 -1.5rem 0 -1.5rem;
|
||||
list-style: none;
|
||||
gap: 0rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.examples__list {
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.examples__list {
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.examples__list__item {
|
||||
padding: 8px 12px;
|
||||
margin: 0px -12px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.examples__list__item__link {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.examples__list__item a {
|
||||
padding: 8px 12px;
|
||||
margin: 0px -12px;
|
||||
.examples__list__item__active {
|
||||
display: block;
|
||||
padding: 1.5rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.examples__list__item a:hover {
|
||||
text-decoration: underline;
|
||||
.examples__list__item::before {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
inset: 0.5rem;
|
||||
border-radius: 6px;
|
||||
background: #f5f5f5;
|
||||
z-index: -1;
|
||||
opacity: 0;
|
||||
transition: opacity 0.12s ease-in-out;
|
||||
}
|
||||
.examples__list__item:hover::before,
|
||||
.examples__list__item__active::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.examples__list__item h3 {
|
||||
all: unset;
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
margin: 0.25rem 0rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.examples__list__item__details {
|
||||
margin-top: 1rem;
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
opacity: 0;
|
||||
transition: opacity 0.12s ease-in-out;
|
||||
}
|
||||
.examples__list__item__active .examples__list__item__details {
|
||||
position: static;
|
||||
opacity: 1;
|
||||
transition-delay: 0.1s;
|
||||
}
|
||||
.examples__list__item__standalone {
|
||||
color: inherit;
|
||||
opacity: 0.8;
|
||||
padding: 0.5rem;
|
||||
margin: -0.5rem;
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
.examples__list__item__code {
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.examples__list__item__code a {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem 0.5rem;
|
||||
border-radius: 6px;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.12), 0px 1px 3px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.example {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
}
|
||||
.example__info {
|
||||
width: 25vw;
|
||||
max-width: 300px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border-right: 1px solid #e8e8e8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex: none;
|
||||
}
|
||||
@media screen and (max-width: 800px) {
|
||||
.example__info {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.example__logo {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
flex: none;
|
||||
font-size: 1.15rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: start;
|
||||
padding: calc(1rem - 0.5px) 1rem;
|
||||
}
|
||||
.example__logo .examples__lockup {
|
||||
height: 1.5rem;
|
||||
}
|
||||
.example__info__list {
|
||||
flex: auto;
|
||||
margin: 0;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
font-size: 13px;
|
||||
}
|
||||
.example__info__list .examples__list__item {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
.example__info__list .examples__list__item::before {
|
||||
inset-block: 0.25rem;
|
||||
inset-inline: 0.5rem;
|
||||
}
|
||||
.example__info__list h3 {
|
||||
font-size: 14px;
|
||||
}
|
||||
.example__content {
|
||||
flex: 1 1 auto;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.examples__markdown p {
|
||||
all: unset;
|
||||
display: block;
|
||||
}
|
||||
.examples__markdown p:not(:last-child) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.scroll-light {
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
.scroll-light::-webkit-scrollbar {
|
||||
display: block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: inherit;
|
||||
}
|
||||
.scroll-light::-webkit-scrollbar-button {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 10px;
|
||||
}
|
||||
.scroll-light::-webkit-scrollbar-thumb {
|
||||
background-clip: padding-box;
|
||||
width: 0;
|
||||
min-height: 36px;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 6px;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.scroll-light::-webkit-scrollbar-thumb:hover {
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.example__info__list hr {
|
||||
border: none;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
margin: 0;
|
||||
}
|
||||
|
|
1
apps/examples/src/vite-env.d.ts
vendored
Normal file
1
apps/examples/src/vite-env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference types="vite/client" />
|
|
@ -1,9 +1,9 @@
|
|||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
import { defineConfig } from 'vite'
|
||||
import { PluginOption, defineConfig } from 'vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
plugins: [react(), exampleReadmePlugin()],
|
||||
root: path.join(__dirname, 'src'),
|
||||
publicDir: path.join(__dirname, 'public'),
|
||||
build: {
|
||||
|
@ -21,3 +21,81 @@ export default defineConfig({
|
|||
'process.env.TLDRAW_ENV': JSON.stringify(process.env.VERCEL_ENV ?? 'development'),
|
||||
},
|
||||
})
|
||||
|
||||
function exampleReadmePlugin(): PluginOption {
|
||||
return {
|
||||
name: 'example-readme',
|
||||
async transform(src, id) {
|
||||
const match = id.match(/examples\/src\/examples\/(.*)\/README.md$/)
|
||||
if (!match) return
|
||||
|
||||
const remark = (await import('remark')).remark
|
||||
const remarkFrontmatter = (await import('remark-frontmatter')).default
|
||||
const remarkHtml = (await import('remark-html')).default
|
||||
const matter = (await import('vfile-matter')).matter
|
||||
|
||||
const file = await remark()
|
||||
.use(remarkFrontmatter)
|
||||
.use(remarkHtml)
|
||||
.use(() => (_, file) => matter(file))
|
||||
.process(src)
|
||||
|
||||
const frontmatter = parseFrontMatter(file.data.matter, id)
|
||||
|
||||
const separator = '\n<hr>\n'
|
||||
const parts = String(file).split(separator)
|
||||
const description = parts[0]
|
||||
const details = parts.slice(1).join(separator)
|
||||
const path = `/${match[1]}`
|
||||
const codeUrl = `https://github.com/tldraw/tldraw/tree/main/apps/examples/src/examples${path}`
|
||||
|
||||
const result = [
|
||||
`export const title = ${JSON.stringify(frontmatter.title)};`,
|
||||
`export const order = ${JSON.stringify(frontmatter.order)};`,
|
||||
`export const hide = ${JSON.stringify(frontmatter.hide)};`,
|
||||
`export const description = ${JSON.stringify(description)};`,
|
||||
`export const details = ${JSON.stringify(details)};`,
|
||||
`export const codeUrl = ${JSON.stringify(codeUrl)};`,
|
||||
`export const path = ${JSON.stringify(path)};`,
|
||||
`export const componentFile = ${JSON.stringify(frontmatter.component)};`,
|
||||
`import {lazy} from 'react';`,
|
||||
`export const loadComponent = async () => {`,
|
||||
` return (await import(${JSON.stringify(frontmatter.component)})).default;`,
|
||||
`};`,
|
||||
]
|
||||
|
||||
return result.join('\n')
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function parseFrontMatter(data: unknown, fileName: string) {
|
||||
if (!data || typeof data !== 'object') {
|
||||
throw new Error(`Frontmatter missing in ${fileName}`)
|
||||
}
|
||||
|
||||
if (!('title' in data && typeof data.title === 'string')) {
|
||||
throw new Error(`Frontmatter key 'title' must be string in ${fileName}`)
|
||||
}
|
||||
|
||||
if (!('component' in data && typeof data.component === 'string')) {
|
||||
throw new Error(`Frontmatter key 'component' must be string in ${fileName}`)
|
||||
}
|
||||
|
||||
const order = 'order' in data ? data.order : null
|
||||
if (order !== null && typeof order !== 'number') {
|
||||
throw new Error(`Frontmatter key 'order' must be number in ${fileName}`)
|
||||
}
|
||||
|
||||
const hide = 'hide' in data ? data.hide : false
|
||||
if (hide !== false && hide !== true) {
|
||||
throw new Error(`Frontmatter key 'hide' must be boolean in ${fileName}`)
|
||||
}
|
||||
|
||||
return {
|
||||
title: data.title,
|
||||
component: data.component,
|
||||
order,
|
||||
hide,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
"postinstall": "husky install && yarn refresh-assets",
|
||||
"refresh-assets": "lazy refresh-assets",
|
||||
"build": "lazy build",
|
||||
"dev": "lazy run dev --filter='{,bublic/}apps/examples' --filter='{,bublic/}packages/tldraw'",
|
||||
"dev": "LAZYREPO_PRETTY_OUTPUT=0 lazy run dev --filter='{,bublic/}apps/examples' --filter='{,bublic/}packages/tldraw'",
|
||||
"dev-vscode": "code ./apps/vscode/extension && lazy run dev --filter='{,bublic/}apps/vscode/{extension,editor}'",
|
||||
"build-types": "lazy inherit",
|
||||
"build-api": "lazy build-api",
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue