Examples tweaks (#2681)

### Change Type

- [x] `documentation` — Changes to the documentation only[^2]
This commit is contained in:
Steve Ruiz 2024-02-02 17:36:30 +00:00 committed by GitHub
parent 53698bed70
commit 2fd6f254c8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 694 additions and 646 deletions

View file

@ -9,7 +9,7 @@ declare const EDITOR_C: Editor
test.describe('Focus', () => { test.describe('Focus', () => {
test('focus events', async ({ page }) => { test('focus events', async ({ page }) => {
await page.goto('http://localhost:5420/multiple') await page.goto('http://localhost:5420/multiple/full')
await page.waitForSelector('.tl-canvas') await page.waitForSelector('.tl-canvas')
const EditorA = (await page.$(`.A`))! const EditorA = (await page.$(`.A`))!
@ -83,7 +83,7 @@ test.describe('Focus', () => {
}) })
test('kbds when not focused', async ({ page }) => { test('kbds when not focused', async ({ page }) => {
await page.goto('http://localhost:5420/multiple') await page.goto('http://localhost:5420/multiple/full')
await page.waitForSelector('.tl-canvas') await page.waitForSelector('.tl-canvas')
// Should not have any shapes on the page // Should not have any shapes on the page
@ -107,7 +107,7 @@ test.describe('Focus', () => {
}) })
test('kbds when focused', async ({ page }) => { test('kbds when focused', async ({ page }) => {
await page.goto('http://localhost:5420/multiple') await page.goto('http://localhost:5420/multiple/full')
await page.waitForSelector('.tl-canvas') await page.waitForSelector('.tl-canvas')
const EditorA = (await page.$(`.A`))! const EditorA = (await page.$(`.A`))!

View file

@ -12,67 +12,67 @@ test.describe('Routes', () => {
}) })
test('api', async ({ page }) => { test('api', async ({ page }) => {
await page.goto('http://localhost:5420/api') await page.goto('http://localhost:5420/api/full')
await page.waitForSelector('.tl-canvas') await page.waitForSelector('.tl-canvas')
}) })
test('hide-ui', async ({ page }) => { test('hide-ui', async ({ page }) => {
await page.goto('http://localhost:5420/custom-config') await page.goto('http://localhost:5420/custom-config/full')
await page.waitForSelector('.tl-canvas') await page.waitForSelector('.tl-canvas')
}) })
test('custom-config', async ({ page }) => { test('custom-config', async ({ page }) => {
await page.goto('http://localhost:5420/custom-config') await page.goto('http://localhost:5420/custom-config/full')
await page.waitForSelector('.tl-canvas') await page.waitForSelector('.tl-canvas')
}) })
test('custom-ui', async ({ page }) => { test('custom-ui', async ({ page }) => {
await page.goto('http://localhost:5420/custom-ui') await page.goto('http://localhost:5420/custom-ui/full')
await page.waitForSelector('.tl-canvas') await page.waitForSelector('.tl-canvas')
}) })
test('exploded', async ({ page }) => { test('exploded', async ({ page }) => {
await page.goto('http://localhost:5420/exploded') await page.goto('http://localhost:5420/exploded/full')
await page.waitForSelector('.tl-canvas') await page.waitForSelector('.tl-canvas')
}) })
test('scroll', async ({ page }) => { test('scroll', async ({ page }) => {
await page.goto('http://localhost:5420/scroll') await page.goto('http://localhost:5420/scroll/full')
await page.waitForSelector('.tl-canvas') await page.waitForSelector('.tl-canvas')
}) })
test('multiple', async ({ page }) => { test('multiple', async ({ page }) => {
await page.goto('http://localhost:5420/multiple') await page.goto('http://localhost:5420/multiple/full')
await page.waitForSelector('.tl-canvas') await page.waitForSelector('.tl-canvas')
}) })
test('error-boundary', async ({ page }) => { test('error-boundary', async ({ page }) => {
await page.goto('http://localhost:5420/error-boundary') await page.goto('http://localhost:5420/error-boundary/full')
await page.waitForSelector('.tl-canvas') await page.waitForSelector('.tl-canvas')
}) })
test('user-presence', async ({ page }) => { test('user-presence', async ({ page }) => {
await page.goto('http://localhost:5420/user-presence') await page.goto('http://localhost:5420/user-presence/full')
await page.waitForSelector('.tl-canvas') await page.waitForSelector('.tl-canvas')
}) })
test('ui-events', async ({ page }) => { test('ui-events', async ({ page }) => {
await page.goto('http://localhost:5420/ui-events') await page.goto('http://localhost:5420/ui-events/full')
await page.waitForSelector('.tl-canvas') await page.waitForSelector('.tl-canvas')
}) })
test('store-events', async ({ page }) => { test('store-events', async ({ page }) => {
await page.goto('http://localhost:5420/store-events') await page.goto('http://localhost:5420/store-events/full')
await page.waitForSelector('.tl-canvas') await page.waitForSelector('.tl-canvas')
}) })
test('persistence', async ({ page }) => { test('persistence', async ({ page }) => {
await page.goto('http://localhost:5420/persistence') await page.goto('http://localhost:5420/persistence/full')
await page.waitForSelector('.tl-canvas') await page.waitForSelector('.tl-canvas')
}) })
test('snapshots', async ({ page }) => { test('snapshots', async ({ page }) => {
await page.goto('http://localhost:5420/snapshots') await page.goto('http://localhost:5420/snapshots/full')
await page.waitForSelector('.tl-canvas') await page.waitForSelector('.tl-canvas')
}) })
}) })

View file

@ -34,7 +34,7 @@
}, },
"dependencies": { "dependencies": {
"@playwright/test": "^1.38.1", "@playwright/test": "^1.38.1",
"@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.5",
"@tldraw/assets": "workspace:*", "@tldraw/assets": "workspace:*",
"@tldraw/tldraw": "workspace:*", "@tldraw/tldraw": "workspace:*",
"@vercel/analytics": "^1.1.1", "@vercel/analytics": "^1.1.1",

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 59 KiB

View file

@ -1,12 +1,25 @@
import * as Accordion from '@radix-ui/react-accordion' import * as Dialog from '@radix-ui/react-alert-dialog'
import { assert, assertExists } from '@tldraw/tldraw' import { Dispatch, createContext, useContext, useState } from 'react'
import { useEffect, useRef } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { ExamplesLink } from './components/ExamplesLink'
import ExamplesTldrawLogo from './components/ExamplesTldrawLogo'
import { Chevron } from './components/Icons'
import { Example, examples } from './examples' import { Example, examples } from './examples'
const dialogContext = createContext<{
example: Example | null
setExampleDialog: Dispatch<Example | null>
}>({
example: null,
setExampleDialog: () => void null,
})
export function DialogContextProvider({ children }: { children: React.ReactNode }) {
const [example, setExampleDialog] = useState<Example | null>(null)
return (
<dialogContext.Provider value={{ example, setExampleDialog }}>
{children}
</dialogContext.Provider>
)
}
export function ExamplePage({ export function ExamplePage({
example, example,
children, children,
@ -14,120 +27,176 @@ export function ExamplePage({
example: Example example: Example
children: React.ReactNode children: React.ReactNode
}) { }) {
const scrollElRef = useRef<HTMLDivElement>(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])
const categories = examples.map((e) => e.id) const categories = examples.map((e) => e.id)
return ( return (
<DialogContextProvider>
<div className="example"> <div className="example">
<div className="example__info"> <div className="example__sidebar scroll-light">
<div className="example__sidebar-header"> <div className="example__sidebar__header">
<Link className="example__logo" to="/"> <Link className="example__sidebar__header__logo" to="/">
<ExamplesTldrawLogo /> <TldrawLogo />
</Link> </Link>
<div className="example__info__list__socials"> <div className="example__sidebar__header__socials">
<a <a
target="_blank"
href="https://twitter.com/tldraw" href="https://twitter.com/tldraw"
className="example__info__list__socials__button"
title="twitter" title="twitter"
className="hoverable"
> >
<SocialIcon icon="twitter" /> <SocialIcon icon="twitter" />
</a> </a>
<a <a
target="_blank"
href="https://github.com/tldraw/tldraw" href="https://github.com/tldraw/tldraw"
className="example__info__list__socials__button"
title="github" title="github"
className="hoverable"
> >
<SocialIcon icon="github" /> <SocialIcon icon="github" />
</a> </a>
<a <a
target="_blank"
href="https://discord.com/invite/SBBEVCA4PG" href="https://discord.com/invite/SBBEVCA4PG"
className="example__info__list__socials__button"
title="discord" title="discord"
className="hoverable"
> >
<SocialIcon icon="discord" /> <SocialIcon icon="discord" />
</a> </a>
</div> </div>
</div> </div>
<Accordion.Root <ul className="example__sidebar__categories scroll-light">
type="multiple"
defaultValue={categories}
className="example__info__list scroll-light"
ref={scrollElRef}
>
{categories.map((currentCategory) => ( {categories.map((currentCategory) => (
<Accordion.Item key={currentCategory} value={currentCategory}> <li key={currentCategory} className="example__sidebar__category">
<Accordion.Trigger className="accordion__trigger"> <h3 className="example__sidebar__category__header">{currentCategory}</h3>
<div className="examples__list__item accordion__trigger__container"> <ul className="example__sidebar__category__items">
<h3 className="accordion__trigger__heading">{currentCategory}</h3>
<Chevron />
</div>
</Accordion.Trigger>
<Accordion.Content className="accordion__content">
<span className="accordion__content__separator"></span>
<div className="accordion__content__examples">
{examples {examples
.find((category) => category.id === currentCategory) .find((category) => category.id === currentCategory)
?.value.map((sidebarExample) => ( ?.value.map((sidebarExample) => (
<ExamplesLink <ExampleSidebarListItem
key={sidebarExample.path} key={sidebarExample.path}
example={sidebarExample} example={sidebarExample}
isActive={sidebarExample.path === example.path} isActive={sidebarExample.path === example.path}
ref={sidebarExample.path === example.path ? activeElRef : undefined}
/> />
))} ))}
</div> </ul>
</Accordion.Content> </li>
</Accordion.Item>
))} ))}
</Accordion.Root> </ul>
<div className="example__info__list__link"> <div className="example__sidebar__footer-links">
<a <a
className="link__button link__button--grey" className="example__sidebar__footer-link example__sidebar__footer-link--grey"
target="_blank" target="_blank"
href="https://github.com/tldraw/tldraw/issues/new?assignees=&labels=Example%20Request&projects=&template=example_request.yml&title=%5BExample Request%5D%3A+" href="https://github.com/tldraw/tldraw/issues/new?assignees=&labels=Example%20Request&projects=&template=example_request.yml&title=%5BExample Request%5D%3A+"
> >
Request an example Request an example
</a> </a>
<a className="link__button link__button--grey" target="_blank" href="https://tldraw.dev"> <a
className="example__sidebar__footer-link example__sidebar__footer-link--grey"
target="_blank"
href="https://tldraw.dev"
>
Visit the docs Visit the docs
</a> </a>
</div> </div>
</div> </div>
<div className="example__content">{children}</div> <div className="example__content">
{children}
<Dialogs />
</div> </div>
</div>
</DialogContextProvider>
) )
} }
export function SocialIcon({ icon }: { icon: string }) { function ExampleSidebarListItem({
example,
isActive,
}: {
example: Example
isActive?: boolean
showDescriptionWhenInactive?: boolean
}) {
const { setExampleDialog } = useContext(dialogContext)
return (
<li className="examples__sidebar__item" data-active={isActive}>
<Link to={example.path} className="examples__sidebar__item__link hoverable" />
<div className="examples__sidebar__item__title">
<span>{example.title}</span>
</div>
{isActive && (
<div className="example__sidebar__item__buttons">
{example.details && (
<button
className="example__sidebar__item__button hoverable"
onClick={() => setExampleDialog(example)}
>
<InfoIcon />
</button>
)}
<Link
to={`${example.path}/full`}
className="example__sidebar__item__button hoverable"
aria-label="Standalone"
title="View standalone example"
>
<StandaloneIcon />
</Link>
</div>
)}
</li>
)
}
function Markdown({
sanitizedHtml,
className = '',
}: {
sanitizedHtml: string
className?: string
}) {
return (
<div
className={`examples__markdown ${className}`}
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
/>
)
}
function Dialogs() {
const { example, setExampleDialog } = useContext(dialogContext)
if (!example) return null
const handleOpenChange = (open: boolean) => {
if (!open) {
setExampleDialog(null)
}
}
return (
<Dialog.Root defaultOpen onOpenChange={handleOpenChange} open={!!example}>
<Dialog.Overlay
className="example__dialog__overlay"
onPointerDown={() => setExampleDialog(null)}
/>
<Dialog.Content className="example__dialog__content">
<h1>{example.title}</h1>
<Markdown sanitizedHtml={example.details} className="example__dialog__markdown" />
<div className="example__dialog__actions">
<a href={example.codeUrl}>
View Source <ExternalLinkIcon />
</a>
<Dialog.Cancel className="example__dialog__close">Close</Dialog.Cancel>
</div>
</Dialog.Content>
</Dialog.Root>
)
}
function SocialIcon({ icon }: { icon: string }) {
return ( return (
<img <img
className="icon" className="example__sidebar__icon"
src={`/icons/${icon}.svg`} src={`/icons/${icon}.svg`}
style={{ style={{
mask: `url(/icons/${icon}.svg) center 100% / 100% no-repeat`, mask: `url(/icons/${icon}.svg) center 100% / 100% no-repeat`,
@ -136,3 +205,46 @@ export function SocialIcon({ icon }: { icon: string }) {
/> />
) )
} }
function StandaloneIcon() {
return (
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M2 2.5C2 2.22386 2.22386 2 2.5 2H5.5C5.77614 2 6 2.22386 6 2.5C6 2.77614 5.77614 3 5.5 3H3V5.5C3 5.77614 2.77614 6 2.5 6C2.22386 6 2 5.77614 2 5.5V2.5ZM9 2.5C9 2.22386 9.22386 2 9.5 2H12.5C12.7761 2 13 2.22386 13 2.5V5.5C13 5.77614 12.7761 6 12.5 6C12.2239 6 12 5.77614 12 5.5V3H9.5C9.22386 3 9 2.77614 9 2.5ZM2.5 9C2.77614 9 3 9.22386 3 9.5V12H5.5C5.77614 12 6 12.2239 6 12.5C6 12.7761 5.77614 13 5.5 13H2.5C2.22386 13 2 12.7761 2 12.5V9.5C2 9.22386 2.22386 9 2.5 9ZM12.5 9C12.7761 9 13 9.22386 13 9.5V12.5C13 12.7761 12.7761 13 12.5 13H9.5C9.22386 13 9 12.7761 9 12.5C9 12.2239 9.22386 12 9.5 12H12V9.5C12 9.22386 12.2239 9 12.5 9Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
/>
</svg>
)
}
function InfoIcon() {
return (
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M7.49991 0.876892C3.84222 0.876892 0.877075 3.84204 0.877075 7.49972C0.877075 11.1574 3.84222 14.1226 7.49991 14.1226C11.1576 14.1226 14.1227 11.1574 14.1227 7.49972C14.1227 3.84204 11.1576 0.876892 7.49991 0.876892ZM1.82707 7.49972C1.82707 4.36671 4.36689 1.82689 7.49991 1.82689C10.6329 1.82689 13.1727 4.36671 13.1727 7.49972C13.1727 10.6327 10.6329 13.1726 7.49991 13.1726C4.36689 13.1726 1.82707 10.6327 1.82707 7.49972ZM8.24992 4.49999C8.24992 4.9142 7.91413 5.24999 7.49992 5.24999C7.08571 5.24999 6.74992 4.9142 6.74992 4.49999C6.74992 4.08577 7.08571 3.74999 7.49992 3.74999C7.91413 3.74999 8.24992 4.08577 8.24992 4.49999ZM6.00003 5.99999H6.50003H7.50003C7.77618 5.99999 8.00003 6.22384 8.00003 6.49999V9.99999H8.50003H9.00003V11H8.50003H7.50003H6.50003H6.00003V9.99999H6.50003H7.00003V6.99999H6.50003H6.00003V5.99999Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
/>
</svg>
)
}
function ExternalLinkIcon() {
return (
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M3 2C2.44772 2 2 2.44772 2 3V12C2 12.5523 2.44772 13 3 13H12C12.5523 13 13 12.5523 13 12V8.5C13 8.22386 12.7761 8 12.5 8C12.2239 8 12 8.22386 12 8.5V12H3V3L6.5 3C6.77614 3 7 2.77614 7 2.5C7 2.22386 6.77614 2 6.5 2H3ZM12.8536 2.14645C12.9015 2.19439 12.9377 2.24964 12.9621 2.30861C12.9861 2.36669 12.9996 2.4303 13 2.497L13 2.5V2.50049V5.5C13 5.77614 12.7761 6 12.5 6C12.2239 6 12 5.77614 12 5.5V3.70711L6.85355 8.85355C6.65829 9.04882 6.34171 9.04882 6.14645 8.85355C5.95118 8.65829 5.95118 8.34171 6.14645 8.14645L11.2929 3H9.5C9.22386 3 9 2.77614 9 2.5C9 2.22386 9.22386 2 9.5 2H12.4999H12.5C12.5678 2 12.6324 2.01349 12.6914 2.03794C12.7504 2.06234 12.8056 2.09851 12.8536 2.14645Z"
fill="currentColor"
fillRule="evenodd"
clipRule="evenodd"
/>
</svg>
)
}
function TldrawLogo() {
return <img className="examples__tldraw__logo" src="tldraw_dev_light.png" />
}

View file

@ -1,95 +0,0 @@
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/useMergedRefs'
import { StandaloneIcon } from './Icons'
import { Markdown } from './Markdown'
export const ExamplesLink = forwardRef(function ListLink(
{
example,
isActive,
showDescriptionWhenInactive,
}: { example: Example; isActive?: boolean; showDescriptionWhenInactive?: 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 className="examples__list__item__heading" 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>
{showDescriptionWhenInactive && <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}>
{!showDescriptionWhenInactive && <Markdown sanitizedHtml={example.description} />}
<Markdown sanitizedHtml={example.details} />
<div className="examples__list__item__code">
<a className="link__button" 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>
)
return (
<span
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}
</span>
)
})

View file

@ -1,47 +0,0 @@
export default function ExamplesTldrawLogo() {
return (
<svg
width="722"
height="184"
viewBox="0 0 722 184"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="examples__lockup"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M0 20.2198C0 9.0527 8.67154 0 19.3684 0H164.632C175.328 0 184 9.0527 184 20.2198V163.78C184 174.947 175.328 184 164.632 184H19.3684C8.67154 184 0 174.947 0 163.78V20.2198ZM108.625 52.6242C108.625 57.0709 107.105 60.8439 104.066 63.9432C101.026 67.0424 97.3253 68.5921 92.9639 68.5921C88.4704 68.5921 84.7038 67.0424 81.6641 63.9432C78.6244 60.8439 77.1045 57.0709 77.1045 52.6242C77.1045 48.1774 78.6244 44.4044 81.6641 41.3051C84.7038 38.2059 88.4704 36.6562 92.9639 36.6562C97.3253 36.6562 101.026 38.2059 104.066 41.3051C107.105 44.4044 108.625 48.1774 108.625 52.6242ZM76.9062 108.585C76.9062 104.138 78.4261 100.365 81.4658 97.2657C84.6377 94.0317 88.4704 92.4147 92.9639 92.4147C97.1931 92.4147 100.894 94.0317 104.066 97.2657C107.237 100.365 109.088 103.868 109.616 107.776C110.674 115.053 109.352 122.262 105.651 129.404C102.083 136.545 96.9288 142.003 90.1885 145.776C86.488 147.932 83.4483 147.864 81.0694 145.574C78.8226 143.418 79.4834 140.857 83.0518 137.893C85.0342 136.411 86.6862 134.524 88.0079 132.233C89.3295 129.943 90.1885 127.584 90.585 125.159C90.7172 124.081 90.2546 123.542 89.1973 123.542C86.5541 123.407 83.8447 121.925 81.0694 119.095C78.294 116.265 76.9062 112.762 76.9062 108.585Z"
fill="black"
/>
<path
d="M349.15 149C350.25 149 350.8 148.45 350.8 147.35V124.95C350.8 122.741 349.009 120.95 346.8 120.95H343.9C343 120.95 342.3 120.7 341.8 120.2C341.4 119.8 341.2 119.2 341.2 118.4V38.15C341.2 37.05 340.65 36.5 339.55 36.5H314.3C312.091 36.5 310.3 38.2909 310.3 40.5V127.4C310.3 134.6 312.35 140 316.45 143.6C320.55 147.2 325.8 149 332.2 149H349.15Z"
fill="black"
/>
<path
d="M297.4 124.8C297.4 122.591 295.609 120.8 293.4 120.8H282.85C279.45 120.8 277.15 120.2 275.95 119C274.45 117.6 273.7 115.35 273.7 112.25V99C273.7 96.7909 275.491 95 277.7 95H293.4C295.609 95 297.4 93.2092 297.4 91V68.6C297.4 67.5 296.85 66.95 295.75 66.95H277.7C275.491 66.95 273.7 65.1591 273.7 62.95V45.65C273.7 44.55 273.15 44 272.05 44H247.7C245.491 44 243.7 45.7909 243.7 48V62.95C243.7 65.1591 241.909 66.95 239.7 66.95H236C233.791 66.95 232 68.7409 232 70.95V93.35C232 94.45 232.55 95 233.65 95H239.7C241.909 95 243.7 96.7909 243.7 99V114.5C243.7 126.1 246.75 134.75 252.85 140.45C258.95 146.15 267.8 149 279.4 149H295.75C296.85 149 297.4 148.45 297.4 147.35V124.8Z"
fill="black"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M385.241 65.45C389.841 65.45 393.791 66.6 397.091 68.9C400.199 71.0021 402.763 73.7307 404.785 77.0858C405.139 77.6741 405.768 78.05 406.454 78.05C407.497 78.05 408.341 77.2052 408.341 76.1631V40.5C408.341 38.2909 410.132 36.5 412.341 36.5H436.691C437.791 36.5 438.341 37.05 438.341 38.15V145C438.341 147.209 436.551 149 434.341 149H409.991C408.891 149 408.341 148.45 408.341 147.35V138.341C408.341 137.269 407.473 136.4 406.401 136.4C405.741 136.4 405.128 136.738 404.759 137.285C402.205 141.08 399.349 144.185 396.191 146.6C392.791 149.2 388.541 150.5 383.441 150.5C376.541 150.5 371.091 148.15 367.091 143.45C363.091 138.75 361.091 132.45 361.091 124.55V93.05C361.091 84.45 363.141 77.7 367.241 72.8C371.441 67.9 377.441 65.45 385.241 65.45ZM392.741 122C393.841 123.2 395.791 123.8 398.591 123.8C401.42 123.8 403.907 123.458 406.051 122.774C407.495 122.314 408.341 120.893 408.341 119.378V95.6407C408.341 94.3033 407.69 93.0183 406.484 92.4394C405.539 91.9857 404.558 91.6392 403.541 91.4C401.841 90.9 400.191 90.65 398.591 90.65C393.491 90.65 390.941 93.2 390.941 98.3V116.3C390.941 119 391.541 120.9 392.741 122Z"
fill="black"
/>
<path
d="M482.325 149C484.534 149 486.325 147.209 486.325 145V102.793C486.325 101.4 487.039 100.083 488.293 99.4756C489.524 98.8794 490.818 98.3876 492.175 98C494.275 97.3 496.625 96.95 499.225 96.95C501.125 96.95 502.925 97.1 504.625 97.4C506.297 97.6787 507.926 98.0437 509.512 98.495C509.753 98.5636 510.002 98.6 510.252 98.6C511.701 98.6 512.875 97.4259 512.875 95.9776V68.6C512.875 67.9 512.625 67.3 512.125 66.8C511.825 66.5 511.275 66.2 510.475 65.9C509.675 65.6 508.475 65.45 506.875 65.45C502.175 65.45 498.425 66.9 495.625 69.8C493.144 72.3728 491.212 76.1275 489.829 81.0641C489.609 81.8457 488.905 82.4 488.094 82.4C487.117 82.4 486.325 81.608 486.325 80.6311V68.6C486.325 67.5 485.775 66.95 484.675 66.95H459.875C457.666 66.95 455.875 68.7409 455.875 70.95V147.35C455.875 148.45 456.425 149 457.525 149H482.325Z"
fill="black"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M596.651 96.2V120.5C596.651 121.732 597.076 122.538 597.928 122.918C598.168 123.025 598.416 123.118 598.665 123.205C598.944 123.302 599.222 123.35 599.501 123.35H599.701C601.91 123.35 603.701 125.141 603.701 127.35V147.35C603.701 148.45 603.151 149 602.051 149H592.001C587.101 149 582.601 148 578.501 146C574.942 144.22 572.214 141.649 570.317 138.286C569.883 137.515 569.087 137 568.202 137C567.342 137 566.561 137.486 566.133 138.233C564.007 141.945 561.23 144.884 557.801 147.05C554.101 149.35 549.351 150.5 543.551 150.5C536.551 150.5 531.001 148.6 526.901 144.8C522.801 141 520.751 135.85 520.751 129.35V123.5C520.751 116.5 522.801 111 526.901 107C531.101 103 537.001 101 544.601 101H565.601C566.761 101 567.701 100.06 567.701 98.9C567.701 95.4 566.751 93.05 564.851 91.85C563.051 90.55 559.551 89.9 554.351 89.9C545.677 89.9 537.101 91.163 528.621 93.6889C528.376 93.762 528.121 93.8 527.864 93.8C526.421 93.8 525.251 92.6299 525.251 91.1864V71.9C525.251 71.2 525.401 70.7 525.701 70.4C526.401 69.7 528.301 68.95 531.401 68.15C534.601 67.35 538.451 66.65 542.951 66.05C547.551 65.35 552.251 65 557.051 65C570.751 65 580.751 67.5 587.051 72.5C593.451 77.4 596.651 85.3 596.651 96.2ZM553.601 128.9C557.953 128.9 562.014 127.917 565.783 125.951C567.003 125.314 567.701 124.015 567.701 122.638V119.55C567.701 117.341 565.91 115.55 563.701 115.55H553.901C552.201 115.55 550.901 116 550.001 116.9C549.201 117.9 548.801 119.1 548.801 120.5V124.1C548.801 127.3 550.401 128.9 553.601 128.9Z"
fill="black"
/>
<path
d="M657.116 66.95C655.25 66.95 653.632 68.2401 653.218 70.0593C651.092 79.3906 649.129 88.1542 647.327 96.35C645.552 104.854 644.213 112.921 643.311 120.552C643.203 121.461 642.434 122.15 641.519 122.15C640.509 122.15 639.698 121.319 639.714 120.309C639.83 112.623 639.518 104.936 638.777 97.25C638.077 88.95 637.177 79.7 636.077 69.5C635.977 68.4 635.777 67.7 635.477 67.4C635.177 67.1 634.727 66.95 634.127 66.95H612.635C610.151 66.95 608.268 69.1898 608.69 71.6373C610.697 83.2749 612.626 94.9125 614.477 106.55C616.677 119.75 618.877 132.45 621.077 144.65C621.42 146.192 621.799 147.294 622.216 147.955C622.344 148.16 622.517 148.333 622.717 148.469C623.235 148.823 623.789 149 624.377 149H651.14C652.959 149 654.549 147.773 655.009 146.013C655.784 143.045 656.473 140.391 657.077 138.05C657.877 134.95 658.577 131.9 659.177 128.9C659.777 125.9 660.377 122.6 660.977 119C661.609 115.659 662.282 111.625 662.996 106.897C663.13 106.009 663.893 105.35 664.792 105.35C665.7 105.35 666.468 106.022 666.589 106.922C667.199 111.442 667.729 115.319 668.177 118.55C668.777 122.05 669.327 125.15 669.827 127.85C670.427 130.45 671.027 133.1 671.627 135.8C672.227 138.4 672.927 141.4 673.727 144.8C674.156 146.256 674.584 147.309 675.012 147.958C675.145 148.16 675.317 148.333 675.517 148.469C676.035 148.823 676.589 149 677.177 149H704.405C706.354 149 708.018 147.597 708.348 145.677L714.677 108.8C716.977 95.4 719.227 82.1 721.427 68.9C721.527 67.6 721.077 66.95 720.077 66.95H697.008C694.948 66.95 693.225 68.514 693.028 70.5639C692.167 79.5263 691.383 88.0217 690.677 96.05C690.028 104.399 689.723 112.489 689.762 120.322C689.767 121.328 688.955 122.15 687.949 122.15C687.026 122.15 686.253 121.455 686.146 120.539C685.238 112.745 683.848 104.732 681.977 96.5C680.077 87.7 677.977 78.4 675.677 68.6C675.377 67.5 674.777 66.95 673.877 66.95H657.116Z"
fill="black"
/>
</svg>
)
}

View file

@ -1,34 +0,0 @@
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>
)
}
export function Chevron() {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className="accordion__trigger__chevron"
>
<path
d="M4 6L8 10L12 6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}

View file

@ -1,16 +0,0 @@
import classNames from 'classnames'
export function Markdown({
sanitizedHtml,
className = '',
}: {
sanitizedHtml: string
className?: string
}) {
return (
<div
className={classNames('examples__markdown', className)}
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
/>
)
}

View file

@ -1,19 +0,0 @@
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>
)
}

View file

@ -1,5 +1,5 @@
--- ---
title: Editor API title: Controlling the Editor
component: ./APIExample.tsx component: ./APIExample.tsx
category: editor-api category: editor-api
priority: 1 priority: 1

View file

@ -4,7 +4,7 @@ import '@tldraw/tldraw/tldraw.css'
export default function BasicExample() { export default function BasicExample() {
return ( return (
<div className="tldraw__editor"> <div className="tldraw__editor">
<Tldraw persistenceKey="tldraw_example" /> <Tldraw />
</div> </div>
) )
} }

View file

@ -0,0 +1,14 @@
---
title: Tldraw component
component: ./BasicExample.tsx
category: basic
priority: 1
---
...
---
The `Tldraw` component provides the tldraw editor as a regular React component. You can put this component anywhere in your React project. In this example, we make the component take up the height and width of the container.
By default, the component does not persist between refreshes or sync locally between tabs. To keep your work after a refresh, check the [`persistenceKey`](/peristence-key) example.

View file

@ -9,7 +9,7 @@ DefaultColorThemePalette.lightMode.black.solid = 'aqua'
export default function ChangingDefaultColorsExample() { export default function ChangingDefaultColorsExample() {
return ( return (
<div className="tldraw__editor"> <div className="tldraw__editor">
<Tldraw persistenceKey="tldraw_example" /> <Tldraw persistenceKey="example" />
</div> </div>
) )
} }

View file

@ -1,12 +0,0 @@
---
title: Basic
component: ./BasicExample.tsx
category: basic
priority: 1
---
The easiest way to get started with tldraw.
---
The simplest use of the `<Tldraw/>` component.

View file

@ -4,7 +4,7 @@ import '@tldraw/tldraw/tldraw.css'
export default function ForceMobileExample() { export default function ForceMobileExample() {
return ( return (
<div className="tldraw__editor"> <div className="tldraw__editor">
<Tldraw persistenceKey="tldraw_example" forceMobile /> <Tldraw persistenceKey="example" forceMobile />
</div> </div>
) )
} }

View file

@ -1,8 +1,8 @@
--- ---
title: Force mobile breakpoint title: Force mobile
component: ./ForceBreakpointExample component: ./ForceBreakpointExample
category: basic category: basic
priority: 3 priority: 4
--- ---
Force the editor UI to render as if it were on a mobile device. Force the editor UI to render as if it were on a mobile device.

View file

@ -1,8 +1,7 @@
--- ---
title: Hide UI title: Hide UI
component: ./HideUiExample.tsx component: ./HideUiExample.tsx
category: basic category: ui
priority: 2
--- ---
Hide tldraw's UI with the `hideUi` prop. Hide tldraw's UI with the `hideUi` prop.

View file

@ -0,0 +1,10 @@
import { Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
export default function InferDarkModeExample() {
return (
<div className="tldraw__editor">
<Tldraw inferDarkMode />
</div>
)
}

View file

@ -0,0 +1,12 @@
---
title: Infer dark mode
component: ./InferDarkModeExample.tsx
category: basic
priority: 5
---
...
---
When the `Tldraw` component's `inferDarkMode` is true, the editor will infer its initial theme based on the user's system preferences, if available. You can toggle the dark mode by pressing `Command + /`.

View file

@ -0,0 +1,12 @@
import { Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
export default function InsetExample() {
return (
<div style={{ position: 'absolute', inset: 200 }}>
<div className="tldraw__editor">
<Tldraw />
</div>
</div>
)
}

View file

@ -0,0 +1,8 @@
---
title: Inset editor
component: ./InsetExample.tsx
category: basic
priority: 5
---
The `<Tldraw/>` component may be placed in any layout, even if it's not full-screen. In this example, the editor is inset within the screen layout. The component's interactions should still behave as you'd expect them to.

View file

@ -7,7 +7,7 @@ export default function OnCreateShapeMetaExample() {
return ( return (
<div className="tldraw__editor"> <div className="tldraw__editor">
<Tldraw <Tldraw
persistenceKey="tldraw_example" persistenceKey="example"
onMount={(editor) => { onMount={(editor) => {
//[1] //[1]
editor.getInitialMetaForShape = (_shape) => { editor.getInitialMetaForShape = (_shape) => {

View file

@ -2,7 +2,7 @@
title: Multiple editors title: Multiple editors
component: ./MultipleExample.tsx component: ./MultipleExample.tsx
category: basic category: basic
priority: 3 priority: 7
--- ---
Use multiple `<Tldraw/>` components on the same page. Use multiple `<Tldraw/>` components on the same page.

View file

@ -0,0 +1,10 @@
import { Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
export default function PersistenceKeyExample() {
return (
<div className="tldraw__editor">
<Tldraw persistenceKey="example" />
</div>
)
}

View file

@ -0,0 +1,12 @@
---
title: Persistence key
component: ./PersistenceKeyExample.tsx
category: basic
priority: 2
---
...
---
If the `persistenceKey` prop is provided to the `<Tldraw/>` component, the editor will persist its data locally under that key.

View file

@ -1,8 +1,7 @@
--- ---
title: Persistence title: Persistence
component: ./PersistenceExample.tsx component: ./PersistenceExample.tsx
category: collaboration category: data/assets
priority: 1
--- ---
Save the contents of the editor Save the contents of the editor

View file

@ -2,11 +2,11 @@
title: Readonly title: Readonly
component: ./ReadOnlyExample component: ./ReadOnlyExample
category: basic category: basic
priority: 2 priority: 3
--- ---
Use the editor in readonly mode. Use the editor in readonly mode.
--- ---
Users can still pan and zoom the canvas, but they can't change the contents. If the `readonly` prop is provide to the `Tldraw` component, the editor won't allow a user to change the contents of the document. It will still allow the user to move the camera, select shapes, copy, and make other superficial changes.

View file

@ -5,7 +5,7 @@ export default function ReadOnlyExample() {
return ( return (
<div className="tldraw__editor"> <div className="tldraw__editor">
<Tldraw <Tldraw
persistenceKey="tldraw_example" persistenceKey="example"
onMount={(editor) => { onMount={(editor) => {
editor.updateInstanceState({ isReadonly: true }) editor.updateInstanceState({ isReadonly: true })
}} }}

View file

@ -2,11 +2,11 @@
title: Scrolling container title: Scrolling container
component: ./ScrollExample.tsx component: ./ScrollExample.tsx
category: basic category: basic
priority: 1 priority: 6
--- ---
Use the editor inside a scrollable container. Use the editor inside a scrollable container.
--- ---
Tldraw doesn't have to be full screen. The `Tldraw` component can be used inside of any layout, including a scrollable container. Note that the editor will accept mousewheel events unless it is "unfocused".

View file

@ -14,7 +14,10 @@ export default function ScrollExample() {
}} }}
> >
<div style={{ width: '60vw', height: '80vh' }}> <div style={{ width: '60vw', height: '80vh' }}>
<Tldraw persistenceKey="scroll-example" /> <Tldraw
persistenceKey="scroll-example"
// autoFocus={false}
/>
</div> </div>
</div> </div>
) )
@ -22,4 +25,6 @@ export default function ScrollExample() {
/* /*
This example shows how you can use the Tldraw component inside a scrollable container. This example shows how you can use the Tldraw component inside a scrollable container.
The component will still accept mousewheel events while "focused". Try turning off the
autoFocus prop to see the difference.
*/ */

View file

@ -4,9 +4,3 @@ component: ./YjsExample.tsx
category: collaboration category: collaboration
priority: 2 priority: 2
--- ---
TODOTODO
---
TODOTODO

View file

@ -9,7 +9,8 @@ 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 { ExamplePage } from './ExamplePage'
import { examples } from './examples' import { examples } from './examples'
import EndToEnd from './testing/end-to-end' import Develop from './misc/develop'
import EndToEnd from './misc/end-to-end'
// This example is only used for end to end tests // This example is only used for end to end tests
@ -20,8 +21,8 @@ setDefaultEditorAssetUrls(assetUrls)
setDefaultUiAssetUrls(assetUrls) setDefaultUiAssetUrls(assetUrls)
const gettingStartedExamples = examples.find((e) => e.id === 'Getting Started') const gettingStartedExamples = examples.find((e) => e.id === 'Getting Started')
if (!gettingStartedExamples) throw new Error('Could not find getting started exmaples') if (!gettingStartedExamples) throw new Error('Could not find getting started exmaples')
const basicExample = gettingStartedExamples.value.find((e) => e.title === 'Basic') const basicExample = gettingStartedExamples.value.find((e) => e.priority === 1)
if (!basicExample) throw new Error('Could not find basic example') if (!basicExample) throw new Error('Could not find initial example')
const router = createBrowserRouter([ const router = createBrowserRouter([
{ {
@ -41,6 +42,10 @@ const router = createBrowserRouter([
} }
}, },
}, },
{
path: 'develop',
element: <Develop />,
},
{ {
path: 'end-to-end', path: 'end-to-end',
element: <EndToEnd />, element: <EndToEnd />,

View file

@ -0,0 +1,10 @@
import { Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/tldraw.css'
export default function Develop() {
return (
<div className="tldraw__editor">
<Tldraw persistenceKey="tldraw_example" />
</div>
)
}

View file

@ -3,6 +3,7 @@
:root { :root {
--gray-light: #f5f5f5; --gray-light: #f5f5f5;
--gray-dark: #e8e8e8; --gray-dark: #e8e8e8;
--black-transparent-lighter: rgba(0, 0, 0, 0.07);
--black-transparent-light: rgba(0, 0, 0, 0.3); --black-transparent-light: rgba(0, 0, 0, 0.3);
--black-transparent-dark: rgba(0, 0, 0, 0.5); --black-transparent-dark: rgba(0, 0, 0, 0.5);
} }
@ -13,7 +14,6 @@ body {
margin: 0; margin: 0;
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
min-height: 100vh; min-height: 100vh;
font-size: 16px;
/* mobile viewport bug fix */ /* mobile viewport bug fix */
min-height: -webkit-fill-available; min-height: -webkit-fill-available;
height: 100%; height: 100%;
@ -38,209 +38,172 @@ html,
touch-action: none; touch-action: none;
} }
.examples { .example {
padding: 1.5rem;
max-width: 1080px;
margin: 0 auto;
}
.examples hr {
border: none;
border-top: 1px solid var(--gray-dark);
margin: 0;
}
.examples__header {
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: 1.875rem;
width: auto;
margin-right: 0.5rem;
}
.examples__list {
display: grid; display: grid;
grid-template-columns: repeat(1, 1fr); grid-template-columns: auto 1fr;
padding: 0; grid-auto-flow: column;
margin: 0 -1.5rem 0 -1.5rem;
} }
@media screen and (max-width: 800px) { .example__content {
.examples__list { position: relative;
grid-template-columns: repeat(1, 1fr); height: 100%;
}
}
@media screen and (max-width: 600px) {
.examples__list {
grid-template-columns: repeat(1, 1fr);
}
}
.accordion__trigger {
border: none;
background: none;
cursor: pointer;
width: 100%; width: 100%;
text-align: left; overflow: auto;
font-size: 10px;
} }
.accordion__trigger__chevron { @media screen and (max-width: 500px) {
color:var(--black-transparent-light); .example {
transition: transform 300ms; display: grid;
grid-template-columns: 1fr;
grid-auto-flow: column;
}
.example__sidebar {
display: none;
}
} }
.accordion__trigger[data-state='closed'] > div > .accordion__trigger__chevron { /* --------------------- Sidebar -------------------- */
transform: rotate(-90deg);
}
.example__sidebar {
.accordion__trigger__heading { width: 256px;
min-width: 256px;
display: grid;
grid-template-rows: auto 1fr auto;
border-right: 1px solid var(--black-transparent-light);
overflow: hidden;
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: var(--black-transparent-dark); max-height: 100%;
margin-top: 0.25rem;
margin-bottom: 0.25rem;
} }
.accordion__content { .example__sidebar a {
text-decoration: none;
color: inherit;
}
/* Header */
.example__sidebar__header {
display: flex; display: flex;
height: 40px;
align-items: center;
max-width: 100%;
padding: 0 4px 0 8px;
}
.example__sidebar__header__logo {
all: unset;
cursor: pointer;
flex: none;
font-size: 1.15rem;
display: flex;
align-items: center;
justify-content: start;
overflow: hidden;
padding: 0px;
}
.examples__tldraw__logo {
height: 18px;
width: auto;
}
/* Categories */
ul.example__sidebar__categories {
list-style: none;
padding: 0;
margin: 0;
overflow: auto;
}
.example__sidebar__category__header {
display: grid;
grid-template-columns: 1fr auto;
font-weight: 800;
align-items: center;
padding: 8px 12px;
margin: 0px;
font-size: 14px;
}
/* Category */
li.example__sidebar__category {
position: relative;
padding: 8px 0px;
}
ul.example__sidebar__category__items {
list-style: none;
padding: 0px 0px 0px 4px;
margin: 0px;
}
li.examples__sidebar__item {
padding: 0px 8px 0px 8px;
position: relative;
margin: 0px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
flex-direction: row; flex-direction: row;
} }
.accordion__content__separator {
border-right: 1px solid var(--gray-dark); .examples__sidebar__item__link {
opacity: 0.5; display: block;
padding-right: 1.5rem; position: absolute;
inset: 0px;
height: 100%;
} }
.accordion__content__examples { .examples__sidebar__item__title {
width: 100%; max-width: 100%;
} font-weight: 500;
margin: 0px;
.examples__list__item {
overflow: hidden; overflow: hidden;
position: relative; white-space: nowrap;
padding: 1.5rem; text-overflow: ellipsis;
display: block; flex-grow: 2;
}
.examples__sidebar__item__details {
width: 100%; width: 100%;
margin: 8px 0 0 0;
} }
.examples__sidebar__item[data-active='true']:not(:hover) > .examples__sidebar__item__link::after {
.examples__list__item__link {
position: absolute;
inset: 0;
z-index: 1;
cursor: pointer;
}
.examples__list__item__active {
padding: 1.5rem;
text-decoration: none;
color: inherit;
position: relative;
width: 100%;
}
.examples__list__item::before {
content: ' ';
position: absolute;
inset: 0.5rem;
border-radius: 6px;
background: var(--gray-light);
z-index: -1;
opacity: 0;
transition: opacity 0.12s ease-in-out;
}
.examples__list__item__active::before {
opacity: 1; opacity: 1;
} }
@media screen and (pointer: fine) {
.examples__list__item:hover::before {
opacity: 1;
}
}
.examples__list__item__heading { /* -------------------- List Item ------------------- */
all: unset;
font-weight: 700;
font-size: 14px;
margin-top: 0.25rem;
display: flex;
align-items: center;
justify-content: space-between;
}
.examples__list__item__details {
position: absolute;
left: 1rem;
right: 1rem;
opacity: 0;
transition: opacity 0.12s ease-in-out;
margin-top: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.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;
}
.link__button {
padding: 0.5rem 0.5rem;
border-radius: 6px;
background-color: hsl(214, 84%, 56%);
display: flex;
flex: 1 1 auto;
text-align: center;
align-items: center;
justify-content: center;
color: white;
text-decoration: none;
box-shadow:
0px 1px 2px rgba(0, 0, 0, 0.12),
0px 1px 3px rgba(0, 0, 0, 0.04);
}
.link__button--grey { .example__sidebar__item__buttons {
background-color: #f5f5f5; display: flex;
flex-direction: row;
height: 32px;
margin-right: -8px;
color: black; color: black;
border: 1px solid #e8e8e8; }
font-size: 14px; .example__sidebar__item__button {
all: unset;
cursor: pointer;
position: relative;
display: block;
opacity: 0.8;
height: 100%;
width: 32px;
z-index: 10;
color: inherit;
display: flex;
align-items: center;
justify-content: center;
}
.example__sidebar__item__button:hover {
z-index: 20;
}
.example__sidebar__item__button:nth-of-type(n + 2) {
margin-left: -4px;
} }
.example { .example {
@ -249,76 +212,75 @@ html,
display: flex; display: flex;
align-items: stretch; align-items: stretch;
} }
.example__info {
width: 300px; /* ----------------- Footer Buttons ----------------- */
position: relative;
z-index: 1; .example__sidebar__footer-links {
border-right: 1px solid var(--gray-dark);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 100%; gap: 8px;
flex: none;
}
@media screen and (max-width: 800px) {
.example__info {
display: none;
}
}
.example__sidebar-header {
display: flex;
height: 44px;
margin-bottom: 8px;
padding-right: 8px;
align-items: center;
}
.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; font-size: 13px;
font-weight: 400;
padding: 8px;
border-top: 1px solid #e8e8e8;
} }
.example__info__list .examples__list__item { .example__sidebar__footer-link {
padding: 0.75rem 1rem; padding: 8px 8px;
} border-radius: 6px;
.example__info__list .examples__list__item::before { background-color: hsl(214, 84%, 56%);
inset-block: 0.25rem; display: flex;
inset-inline: 0.5rem;
}
.example__content {
flex: 1 1 auto; flex: 1 1 auto;
position: relative; text-align: center;
z-index: 0; align-items: center;
overflow: auto; justify-content: center;
color: #fff;
text-decoration: none;
box-shadow:
0px 1px 2px rgba(0, 0, 0, 0.12),
0px 1px 3px rgba(0, 0, 0, 0.04);
} }
.examples__markdown p { .example__sidebar__footer-link > a {
all: unset; color: white;
display: block;
} }
.examples__markdown p:not(:last-child) {
margin-bottom: 0.5rem; .example__sidebar__footer-link--grey {
background-color: #f5f5f5;
color: black;
border: 1px solid #e8e8e8;
font-size: 14px;
} }
/* ------------------ Social Links ------------------ */
.example__sidebar__header__socials {
display: flex;
flex-direction: row;
justify-content: flex-end;
flex-grow: 2;
}
.example__sidebar__header__socials a {
color: black;
height: 32px;
width: 32px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.example__sidebar__icon {
flex-shrink: 0;
width: 16px;
height: 16px;
color: black;
background-color: black;
}
/* --------------------- Scroll --------------------- */
.scroll-light { .scroll-light {
scrollbar-width: thin; scrollbar-width: thin;
} }
@ -348,51 +310,104 @@ html,
background-color: var(--black-transparent-light); background-color: var(--black-transparent-light);
} }
.example__info__list hr { /* ---------------------- Hover --------------------- */
border: none;
border-top: 1px solid var(--gray-dark); .hoverable::after {
margin: 0; display: block;
content: '';
position: absolute;
inset: 1px;
opacity: 0;
z-index: -1;
background-color: var(--gray-light);
border-radius: 4px;
} }
.accordion__trigger__container { .hoverable:hover::after {
opacity: 1;
}
/* --------------------- Dialog --------------------- */
.example__dialog__overlay {
position: fixed;
inset: 0;
z-index: 9999999998;
background-color: var(--black-transparent-dark);
}
.example__dialog__content {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 9999999999;
width: 680px;
max-width: calc(100% - 40px);
max-height: calc(100vh - 40px);
overflow: auto;
border-radius: 8px;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.12);
padding: 16px;
background-color: white;
}
.example__dialog__content h1 {
font-size: 20px;
margin: 0px;
}
.example__dialog__markdown {
min-height: 40px;
line-height: 1.5;
font-size: 16px;
}
.example__dialog__markdown p {
margin: 12px 0px;
}
.example__dialog__markdown a {
color: royalblue;
text-decoration: none;
}
.example__dialog__markdown a:hover {
text-decoration: underline;
}
.example__dialog__actions {
display: flex; display: flex;
flex-direction: row;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
color: var(--black-transparent-dark); margin-top: 24px;
gap: 16px;
} }
.example__info__list__link { .example__dialog__actions a,
display: flex; .example__dialog__actions button {
flex-direction: column; cursor: pointer;
gap: 0.5rem; padding: 8px 12px;
margin-top: 1rem; border-radius: 6px;
font-size: 13px;
font-weight: 400;
padding: 1rem;
border-top: 1px solid #e8e8e8;
}
.example__info__list__socials {
display: flex;
flex-direction: row;
justify-content: flex-end;
flex-grow: 2;
}
.example__info__list__socials__button {
color: black;
height: 32px;
width: 32px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
text-decoration: none;
gap: 8px;
background-color: #f5f5f5;
color: black;
border: 1px solid #e8e8e8;
font-size: 14px;
} }
.icon { .example__dialog__content p > code {
flex-shrink: 0; font-size: 14px;
width: 20px; font-family: monospace;
height: 20px; padding: 1px 4px;
color: black; margin: 0px -2px;
background-color: black; background-color: var(--black-transparent-lighter);
border-radius: 4px;
}
.example__dialog__close {
all: unset;
} }

View file

@ -51,7 +51,7 @@ function exampleReadmePlugin(): PluginOption {
const result = [ const result = [
`export const title = ${JSON.stringify(frontmatter.title)};`, `export const title = ${JSON.stringify(frontmatter.title)};`,
`export const priority = ${JSON.stringify(frontmatter.priority)};`, `export const priority = ${JSON.stringify(frontmatter.priority ?? '100000')};`,
`export const category = ${JSON.stringify(frontmatter.category)};`, `export const category = ${JSON.stringify(frontmatter.category)};`,
`export const hide = ${JSON.stringify(frontmatter.hide)};`, `export const hide = ${JSON.stringify(frontmatter.hide)};`,
`export const description = ${JSON.stringify(description)};`, `export const description = ${JSON.stringify(description)};`,
@ -83,7 +83,7 @@ function parseFrontMatter(data: unknown, fileName: string) {
throw new Error(`Frontmatter key 'component' must be string in ${fileName}`) throw new Error(`Frontmatter key 'component' must be string in ${fileName}`)
} }
const priority = 'priority' in data ? data.priority : null const priority = 'priority' in data ? data.priority : 999999
if (typeof priority !== 'number') { if (typeof priority !== 'number') {
throw new Error(`Frontmatter key 'priority' must be number in ${fileName}`) throw new Error(`Frontmatter key 'priority' must be number in ${fileName}`)
} }

View file

@ -45,7 +45,7 @@
"tldraw.css" "tldraw.css"
], ],
"dependencies": { "dependencies": {
"@radix-ui/react-alert-dialog": "^1.0.0", "@radix-ui/react-alert-dialog": "^1.0.5",
"@radix-ui/react-context-menu": "^2.1.5", "@radix-ui/react-context-menu": "^2.1.5",
"@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-dropdown-menu": "^2.0.6",

View file

@ -4827,7 +4827,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@radix-ui/react-alert-dialog@npm:^1.0.0": "@radix-ui/react-alert-dialog@npm:^1.0.5":
version: 1.0.5 version: 1.0.5
resolution: "@radix-ui/react-alert-dialog@npm:1.0.5" resolution: "@radix-ui/react-alert-dialog@npm:1.0.5"
dependencies: dependencies:
@ -7415,7 +7415,7 @@ __metadata:
resolution: "@tldraw/tldraw@workspace:packages/tldraw" resolution: "@tldraw/tldraw@workspace:packages/tldraw"
dependencies: dependencies:
"@peculiar/webcrypto": "npm:^1.4.0" "@peculiar/webcrypto": "npm:^1.4.0"
"@radix-ui/react-alert-dialog": "npm:^1.0.0" "@radix-ui/react-alert-dialog": "npm:^1.0.5"
"@radix-ui/react-context-menu": "npm:^2.1.5" "@radix-ui/react-context-menu": "npm:^2.1.5"
"@radix-ui/react-dialog": "npm:^1.0.5" "@radix-ui/react-dialog": "npm:^1.0.5"
"@radix-ui/react-dropdown-menu": "npm:^2.0.6" "@radix-ui/react-dropdown-menu": "npm:^2.0.6"
@ -13537,7 +13537,7 @@ __metadata:
resolution: "examples.tldraw.com@workspace:apps/examples" resolution: "examples.tldraw.com@workspace:apps/examples"
dependencies: dependencies:
"@playwright/test": "npm:^1.38.1" "@playwright/test": "npm:^1.38.1"
"@radix-ui/react-accordion": "npm:^1.1.2" "@radix-ui/react-alert-dialog": "npm:^1.0.5"
"@tldraw/assets": "workspace:*" "@tldraw/assets": "workspace:*"
"@tldraw/tldraw": "workspace:*" "@tldraw/tldraw": "workspace:*"
"@vercel/analytics": "npm:^1.1.1" "@vercel/analytics": "npm:^1.1.1"