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);">
|
<div alt style="text-align: center; transform: scale(.5);">
|
||||||
<picture>
|
<picture>
|
||||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/tldraw/tldraw/main/assets/github-hero-dark-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" />
|
<img alt="tldraw" src="https://raw.githubusercontent.com/tldraw/tldraw/main/assets/github-hero-light-2.png"/>
|
||||||
</picture>
|
</picture>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
"@tldraw/assets": "workspace:*",
|
"@tldraw/assets": "workspace:*",
|
||||||
"@tldraw/tldraw": "workspace:*",
|
"@tldraw/tldraw": "workspace:*",
|
||||||
"@vercel/analytics": "^1.0.1",
|
"@vercel/analytics": "^1.0.1",
|
||||||
|
"classnames": "^2.3.2",
|
||||||
"lazyrepo": "0.0.0-alpha.27",
|
"lazyrepo": "0.0.0-alpha.27",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
@ -47,6 +48,10 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-react": "^4.2.0",
|
"@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 { 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 {
|
export const ListLink = forwardRef(function ListLink(
|
||||||
title: string
|
{
|
||||||
route: string
|
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 (
|
return (
|
||||||
<li className="examples__list__item">
|
<li
|
||||||
<Link to={route}>{title}</Link>
|
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>
|
</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 (
|
return (
|
||||||
<div style={{ display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
<div style={{ width: '50vw', height: '100vh' }}>
|
<div style={{ width: '50%', height: '100vh' }}>
|
||||||
<Tldraw
|
<Tldraw
|
||||||
onMount={(editor) => {
|
onMount={(editor) => {
|
||||||
editor.on('event', (event) => handleEvent(event))
|
editor.on('event', (event) => handleEvent(event))
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div
|
||||||
<div
|
style={{
|
||||||
style={{
|
width: '50%',
|
||||||
width: '50vw',
|
height: '100vh',
|
||||||
height: '100vh',
|
padding: 8,
|
||||||
padding: 8,
|
background: '#eee',
|
||||||
background: '#eee',
|
border: 'none',
|
||||||
border: 'none',
|
fontFamily: 'monospace',
|
||||||
fontFamily: 'monospace',
|
fontSize: 12,
|
||||||
fontSize: 12,
|
borderLeft: 'solid 2px #333',
|
||||||
borderLeft: 'solid 2px #333',
|
display: 'flex',
|
||||||
display: 'flex',
|
flexDirection: 'column-reverse',
|
||||||
flexDirection: 'column-reverse',
|
overflow: 'auto',
|
||||||
overflow: 'auto',
|
whiteSpace: 'pre-wrap',
|
||||||
whiteSpace: 'pre-wrap',
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{events.map((t, i) => (
|
||||||
{events.map((t, i) => (
|
<div key={i}>{t}</div>
|
||||||
<div key={i}>{t}</div>
|
))}
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</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
|
// 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.
|
// 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`
|
// 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) => {
|
const handleMount = useCallback((editor: Editor) => {
|
||||||
// Assets are records that store data about shared assets like images, videos, etc.
|
// 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.
|
// Each image has an associated asset record, so we'll create that first.
|
||||||
|
@ -22,7 +22,7 @@ export default function ImageExample() {
|
||||||
typeName: 'asset',
|
typeName: 'asset',
|
||||||
props: {
|
props: {
|
||||||
name: 'tldraw.png',
|
name: 'tldraw.png',
|
||||||
src: '/tldraw.png',
|
src: '/tldraw.png', // You could also use a base64 encoded string here
|
||||||
w: imageWidth,
|
w: imageWidth,
|
||||||
h: imageHeight,
|
h: imageHeight,
|
||||||
mimeType: 'image/png',
|
mimeType: 'image/png',
|
||||||
|
@ -51,7 +51,10 @@ export default function ImageExample() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tldraw__editor">
|
<div className="tldraw__editor">
|
||||||
<Tldraw persistenceKey="tldraw_example" onMount={handleMount} />
|
<Tldraw
|
||||||
|
// persistenceKey="tldraw_local_images_example"
|
||||||
|
onMount={handleMount}
|
||||||
|
/>
|
||||||
</div>
|
</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 } from '@tldraw/tldraw'
|
||||||
import '@tldraw/tldraw/tldraw.css'
|
import '@tldraw/tldraw/tldraw.css'
|
||||||
|
|
||||||
export default function MetaExample() {
|
export default function OnChangeShapeMetaExample() {
|
||||||
return (
|
return (
|
||||||
<div className="tldraw__editor">
|
<div className="tldraw__editor">
|
||||||
<Tldraw
|
<Tldraw
|
||||||
persistenceKey="tldraw_example"
|
persistenceKey="tldraw_example"
|
||||||
onMount={(editor) => {
|
onMount={(editor) => {
|
||||||
// There's no API for setting getInitialMetaForShape yet, but
|
// See the "meta-on-create" example for more about setting the
|
||||||
// you can replace it at runtime like this. This will run for
|
// initial meta for a shape.
|
||||||
// all shapes created by the user.
|
|
||||||
editor.getInitialMetaForShape = (_shape) => {
|
editor.getInitialMetaForShape = (_shape) => {
|
||||||
return {
|
return {
|
||||||
createdBy: editor.user.getId(),
|
|
||||||
createdAt: Date.now(),
|
|
||||||
updatedBy: editor.user.getId(),
|
updatedBy: editor.user.getId(),
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
}
|
}
|
||||||
|
@ -21,14 +18,13 @@ export default function MetaExample() {
|
||||||
// We can also use the sideEffects API to modify a shape before
|
// We can also use the sideEffects API to modify a shape before
|
||||||
// its change is committed to the database. This will run for
|
// its change is committed to the database. This will run for
|
||||||
// all shapes whenever they are updated.
|
// all shapes whenever they are updated.
|
||||||
editor.sideEffects.registerBeforeChangeHandler('shape', (record, _prev, source) => {
|
editor.sideEffects.registerBeforeChangeHandler('shape', (_prev, next, source) => {
|
||||||
if (source !== 'user') return record
|
if (source !== 'user') return next
|
||||||
record.meta = {
|
next.meta = {
|
||||||
...record.meta,
|
|
||||||
updatedBy: editor.user.getId(),
|
updatedBy: editor.user.getId(),
|
||||||
updatedAt: Date.now(),
|
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,
|
TLUiAssetUrlOverrides,
|
||||||
TLUiOverrides,
|
TLUiOverrides,
|
||||||
Tldraw,
|
Tldraw,
|
||||||
|
Vec2d,
|
||||||
toolbarItem,
|
toolbarItem,
|
||||||
useEditor,
|
useEditor,
|
||||||
useValue,
|
useValue,
|
||||||
|
@ -68,7 +69,10 @@ function ScreenshotBox() {
|
||||||
// "page space", i.e. uneffected by scale, and relative to the tldraw
|
// "page space", i.e. uneffected by scale, and relative to the tldraw
|
||||||
// page's top left corner.
|
// page's top left corner.
|
||||||
const zoomLevel = editor.getZoomLevel()
|
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)
|
return new Box2d(x, y, box.w * zoomLevel, box.h * zoomLevel)
|
||||||
},
|
},
|
||||||
[editor]
|
[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() {
|
export default function CustomShapeWithHandles() {
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'fixed', inset: 0 }}>
|
<div style={{ position: 'absolute', inset: 0 }}>
|
||||||
<Tldraw
|
<Tldraw
|
||||||
shapeUtils={shapeUtils}
|
shapeUtils={shapeUtils}
|
||||||
tools={tools}
|
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 (
|
return (
|
||||||
<div style={{ display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
<div style={{ width: '60vw', height: '100vh' }}>
|
<div style={{ width: '60%', height: '100vh' }}>
|
||||||
<Tldraw onMount={setAppToState} />
|
<Tldraw onMount={setAppToState} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div
|
||||||
<div
|
style={{
|
||||||
style={{
|
width: '40%',
|
||||||
width: '40vw',
|
height: '100vh',
|
||||||
height: '100vh',
|
padding: 8,
|
||||||
padding: 8,
|
background: '#eee',
|
||||||
background: '#eee',
|
border: 'none',
|
||||||
border: 'none',
|
fontFamily: 'monospace',
|
||||||
fontFamily: 'monospace',
|
fontSize: 12,
|
||||||
fontSize: 12,
|
borderLeft: 'solid 2px #333',
|
||||||
borderLeft: 'solid 2px #333',
|
display: 'flex',
|
||||||
display: 'flex',
|
flexDirection: 'column-reverse',
|
||||||
flexDirection: 'column-reverse',
|
overflow: 'auto',
|
||||||
overflow: 'auto',
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{storeEvents.map((t, i) => (
|
||||||
{storeEvents.map((t, i) => (
|
<div key={i}>{t}</div>
|
||||||
<div key={i}>{t}</div>
|
))}
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</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 '@tldraw/tldraw/tldraw.css'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
@ -55,7 +62,10 @@ const MyComponentInFront = track(() => {
|
||||||
|
|
||||||
if (!selectionRotatedPageBounds) return null
|
if (!selectionRotatedPageBounds) return null
|
||||||
|
|
||||||
const pageCoordinates = editor.pageToScreen(selectionRotatedPageBounds.point)
|
const pageCoordinates = Vec2d.Sub(
|
||||||
|
editor.pageToScreen(selectionRotatedPageBounds.point),
|
||||||
|
editor.getViewportScreenBounds()
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<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 (
|
return (
|
||||||
<div style={{ display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
<div style={{ width: '60vw', height: '100vh' }}>
|
<div style={{ width: '60%', height: '100vh' }}>
|
||||||
<Tldraw onUiEvent={handleUiEvent} />
|
<Tldraw onUiEvent={handleUiEvent} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div
|
||||||
<div
|
style={{
|
||||||
style={{
|
width: '40%',
|
||||||
width: '40vw',
|
height: '100vh',
|
||||||
height: '100vh',
|
padding: 8,
|
||||||
padding: 8,
|
background: '#eee',
|
||||||
background: '#eee',
|
border: 'none',
|
||||||
border: 'none',
|
fontFamily: 'monospace',
|
||||||
fontFamily: 'monospace',
|
fontSize: 12,
|
||||||
fontSize: 12,
|
borderLeft: 'solid 2px #333',
|
||||||
borderLeft: 'solid 2px #333',
|
display: 'flex',
|
||||||
display: 'flex',
|
flexDirection: 'column-reverse',
|
||||||
flexDirection: 'column-reverse',
|
overflow: 'auto',
|
||||||
overflow: 'auto',
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{uiEvents.map((t, i) => (
|
||||||
{uiEvents.map((t, i) => (
|
<div key={i}>{t}</div>
|
||||||
<div key={i}>{t}</div>
|
))}
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</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,
|
setDefaultEditorAssetUrls,
|
||||||
setDefaultUiAssetUrls,
|
setDefaultUiAssetUrls,
|
||||||
} from '@tldraw/tldraw'
|
} from '@tldraw/tldraw'
|
||||||
import { StrictMode } from 'react'
|
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import { RouterProvider, createBrowserRouter } from 'react-router-dom'
|
import { RouterProvider, createBrowserRouter } from 'react-router-dom'
|
||||||
|
import { ExamplePage } from './ExamplePage'
|
||||||
import ExamplesTldrawLogo from './components/ExamplesTldrawLogo'
|
import { HomePage } from './HomePage'
|
||||||
import { ListLink } from './components/ListLink'
|
import { examples } from './examples'
|
||||||
|
import EndToEnd from './testing/end-to-end'
|
||||||
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'
|
|
||||||
|
|
||||||
// This example is only used for end to end tests
|
// This example is only used for end to end tests
|
||||||
|
|
||||||
|
@ -54,219 +20,50 @@ const assetUrls = getAssetUrlsByMetaUrl()
|
||||||
setDefaultEditorAssetUrls(assetUrls)
|
setDefaultEditorAssetUrls(assetUrls)
|
||||||
setDefaultUiAssetUrls(assetUrls)
|
setDefaultUiAssetUrls(assetUrls)
|
||||||
|
|
||||||
type Example = {
|
const router = createBrowserRouter([
|
||||||
path: string
|
|
||||||
title?: string
|
|
||||||
element: JSX.Element
|
|
||||||
}
|
|
||||||
|
|
||||||
export const allExamples: Example[] = [
|
|
||||||
{
|
{
|
||||||
title: 'Basic (development)',
|
path: '/',
|
||||||
path: 'develop',
|
element: <HomePage />,
|
||||||
element: <BasicExample />,
|
|
||||||
},
|
},
|
||||||
{
|
|
||||||
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',
|
path: 'end-to-end',
|
||||||
element: <EndToEnd />,
|
element: <EndToEnd />,
|
||||||
},
|
},
|
||||||
]
|
...examples.flatMap((example) => [
|
||||||
|
{
|
||||||
function App() {
|
path: example.path,
|
||||||
return (
|
lazy: async () => {
|
||||||
<div className="examples">
|
const Component = await example.loadComponent()
|
||||||
<div className="examples__header">
|
return {
|
||||||
<ExamplesTldrawLogo />
|
element: (
|
||||||
<p>
|
<ExamplePage example={example}>
|
||||||
See docs at <a href="https://tldraw.dev">tldraw.dev</a>
|
<Component />
|
||||||
</p>
|
</ExamplePage>
|
||||||
</div>
|
),
|
||||||
<ul className="examples__list">
|
}
|
||||||
{allExamples
|
},
|
||||||
.filter((example) => example.title)
|
},
|
||||||
.map((example) => (
|
{
|
||||||
<ListLink key={example.path} title={example.title!} route={example.path} />
|
path: `${example.path}/full`,
|
||||||
))}
|
lazy: async () => {
|
||||||
</ul>
|
const Component = await example.loadComponent()
|
||||||
</div>
|
return {
|
||||||
)
|
element: <Component />,
|
||||||
}
|
}
|
||||||
|
},
|
||||||
const router = createBrowserRouter([
|
},
|
||||||
{
|
]),
|
||||||
path: '/',
|
|
||||||
element: <App />,
|
|
||||||
},
|
|
||||||
...allExamples,
|
|
||||||
])
|
])
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const rootElement = document.getElementById('root')!
|
const rootElement = document.getElementById('root')!
|
||||||
const root = createRoot(rootElement!)
|
const root = createRoot(rootElement!)
|
||||||
root.render(
|
root.render(
|
||||||
<StrictMode>
|
<ErrorBoundary
|
||||||
<ErrorBoundary
|
fallback={(error) => <DefaultErrorFallback error={error} />}
|
||||||
fallback={(error) => <DefaultErrorFallback error={error} />}
|
onError={(error) => console.error(error)}
|
||||||
onError={(error) => console.error(error)}
|
>
|
||||||
>
|
<RouterProvider router={router} />
|
||||||
<RouterProvider router={router} />
|
</ErrorBoundary>
|
||||||
</ErrorBoundary>
|
|
||||||
</StrictMode>
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
@ -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,
|
html,
|
||||||
body {
|
body {
|
||||||
|
@ -13,6 +13,12 @@ body {
|
||||||
min-height: -webkit-fill-available;
|
min-height: -webkit-fill-available;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
@media screen and (max-width: 600px) {
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
html,
|
html,
|
||||||
* {
|
* {
|
||||||
|
@ -20,45 +26,258 @@ html,
|
||||||
}
|
}
|
||||||
|
|
||||||
.tldraw__editor {
|
.tldraw__editor {
|
||||||
position: fixed;
|
position: absolute;
|
||||||
inset: 0px;
|
inset: 0px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.examples {
|
.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 {
|
.examples__header {
|
||||||
width: fit-content;
|
padding-bottom: 2rem;
|
||||||
padding-bottom: 32px;
|
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 {
|
.examples__lockup {
|
||||||
height: 56px;
|
height: 1.875rem;
|
||||||
width: auto;
|
width: auto;
|
||||||
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.examples__list {
|
.examples__list {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
grid-template-columns: repeat(1, 1fr);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0 -1.5rem 0 -1.5rem;
|
||||||
list-style: none;
|
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 {
|
.examples__list__item {
|
||||||
padding: 8px 12px;
|
overflow: hidden;
|
||||||
margin: 0px -12px;
|
position: relative;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
.examples__list__item__link {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.examples__list__item a {
|
.examples__list__item__active {
|
||||||
padding: 8px 12px;
|
display: block;
|
||||||
margin: 0px -12px;
|
padding: 1.5rem;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
|
width: 100%;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.examples__list__item a:hover {
|
.examples__list__item::before {
|
||||||
text-decoration: underline;
|
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 react from '@vitejs/plugin-react'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { defineConfig } from 'vite'
|
import { PluginOption, defineConfig } from 'vite'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [react(), exampleReadmePlugin()],
|
||||||
root: path.join(__dirname, 'src'),
|
root: path.join(__dirname, 'src'),
|
||||||
publicDir: path.join(__dirname, 'public'),
|
publicDir: path.join(__dirname, 'public'),
|
||||||
build: {
|
build: {
|
||||||
|
@ -21,3 +21,81 @@ export default defineConfig({
|
||||||
'process.env.TLDRAW_ENV': JSON.stringify(process.env.VERCEL_ENV ?? 'development'),
|
'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",
|
"postinstall": "husky install && yarn refresh-assets",
|
||||||
"refresh-assets": "lazy refresh-assets",
|
"refresh-assets": "lazy refresh-assets",
|
||||||
"build": "lazy build",
|
"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}'",
|
"dev-vscode": "code ./apps/vscode/extension && lazy run dev --filter='{,bublic/}apps/vscode/{extension,editor}'",
|
||||||
"build-types": "lazy inherit",
|
"build-types": "lazy inherit",
|
||||||
"build-api": "lazy build-api",
|
"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