remove svg layer, html all the things, rs to tl (#1227)

This PR has been hijacked! 🗑️🦝🦝🦝

The <Canvas> component was previously split into an <SVGLayer> and an
<HTMLLayer>, mainly due to the complexity around translating SVGs.
However, this was done before we learned that SVGs can have overflow:
visible, so it turns out that we don't really need the SVGLayer at all.
This PR now refactors away SVG Layer.

It also updates the class name prefix in editor from `rs-` to `tl-` and
does a few other small changes.

---------

Co-authored-by: Steve Ruiz <steveruizok@gmail.com>
This commit is contained in:
Lu[ke] Wilson 2023-05-05 07:14:42 -07:00 committed by GitHub
parent 986ffc1dd6
commit dc16ae1b12
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 898 additions and 804 deletions

View file

@ -5,7 +5,7 @@ import '@tldraw/tldraw/ui.css'
const components: Partial<TLEditorComponents> = {
Brush: ({ brush }) => (
<rect
className="rs-brush"
className="tl-brush"
stroke="red"
fill="none"
width={Math.max(1, brush.w)}
@ -29,7 +29,7 @@ const components: Partial<TLEditorComponents> = {
export default function CustomComponentsExample() {
return (
<div className="tldraw__editor">
<Tldraw components={components} />
<Tldraw persistenceKey="custom-components-example" components={components} />
</div>
)
}

View file

@ -0,0 +1,71 @@
import { Tldraw, TLInstance, TLInstancePageState, TLUser, TLUserPresence } from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css'
import '@tldraw/tldraw/ui.css'
import { useRef } from 'react'
export default function UserPresenceExample() {
const rTimeout = useRef<any>(-1)
return (
<div className="tldraw__editor">
<Tldraw
persistenceKey="user-presence-example"
onMount={(app) => {
// There are several records related to user presence that must be
// included for each user. These are created automatically by each
// editor or editor instance, so in a "regular" multiplayer sharing
// all records will include all of these records. In this example,
// we're having to create these ourselves.
const userId = TLUser.createCustomId('user-1')
const user = TLUser.create({
id: userId,
name: 'User 1',
})
const userPresence = TLUserPresence.create({
...app.userPresence,
id: TLUserPresence.createCustomId('user-1'),
cursor: { x: 0, y: 0 },
userId,
})
const instance = TLInstance.create({
...app.instanceState,
id: TLInstance.createCustomId('user-1'),
userId,
})
const instancePageState = TLInstancePageState.create({
...app.pageState,
id: TLInstancePageState.createCustomId('user-1'),
instanceId: TLInstance.createCustomId('instance-1'),
})
app.store.put([user, instance, userPresence, instancePageState])
// Make the fake user's cursor rotate in a circle
if (rTimeout.current) {
clearTimeout(rTimeout.current)
}
rTimeout.current = setInterval(() => {
const SPEED = 0.1
const R = 400
const k = 1000 / SPEED
const t = (Date.now() % k) / k
// rotate in a circle
const x = Math.cos(t * Math.PI * 2) * R
const y = Math.sin(t * Math.PI * 2) * R
app.store.put([
{
...userPresence,
cursor: { x, y },
lastActivityTimestamp: Date.now(),
},
])
}, 100)
}}
/>
</div>
)
}

View file

@ -65,7 +65,7 @@ export default function Example() {
return (
<div className="tldraw__editor">
<Tldraw onMount={handleMount} autoFocus={false}>
<Tldraw persistenceKey="api-example" onMount={handleMount} autoFocus={false}>
<InsideOfAppContext />
</Tldraw>
</div>

View file

@ -114,6 +114,7 @@ export default function Example() {
return (
<div className="tldraw__editor">
<Tldraw
persistenceKey="custom-config"
config={customTldrawConfig}
autoFocus
overrides={{

View file

@ -18,7 +18,7 @@ export default function Example() {
const syncedStore = useLocalSyncClient({
instanceId,
userId: userData.id,
universalPersistenceKey: 'example',
universalPersistenceKey: 'exploded-example',
// config: myConfig // for custom config, see 3-custom-config
})

View file

@ -2,7 +2,7 @@ import { Tldraw } from '@tldraw/tldraw'
import '@tldraw/tldraw/editor.css'
import '@tldraw/tldraw/ui.css'
export default function Example() {
export default function ScrollExample() {
return (
<div
style={{
@ -15,7 +15,7 @@ export default function Example() {
}}
>
<div style={{ width: '60vw', height: '80vh' }}>
<Tldraw autoFocus />
<Tldraw persistenceKey="scroll-example" autoFocus />
</div>
</div>
)

View file

@ -13,6 +13,7 @@ export default function ErrorBoundaryExample() {
return (
<div className="tldraw__editor">
<Tldraw
persistenceKey="error-boundary-example"
components={{
// disable app-level error boundaries:
ErrorFallback: null,

View file

@ -5,7 +5,7 @@ import '@tldraw/tldraw/ui.css'
export default function HideUiExample() {
return (
<div className="tldraw__editor">
<Tldraw autoFocus hideUi />
<Tldraw persistenceKey="hide-ui-example" autoFocus hideUi />
</div>
)
}

View file

@ -10,6 +10,7 @@ import { createRoot } from 'react-dom/client'
import { RouterProvider, createBrowserRouter } from 'react-router-dom'
import ExampleBasic from './1-basic/BasicExample'
import CustomComponentsExample from './10-custom-components/CustomComponentsExample'
import UserPresenceExample from './11-user-presence/UserPresenceExample'
import ExampleApi from './2-api/APIExample'
import CustomConfigExample from './3-custom-config/CustomConfigExample'
import CustomUiExample from './4-custom-ui/CustomUiExample'
@ -70,10 +71,13 @@ export const allExamples: Example[] = [
path: '/custom-components',
element: <CustomComponentsExample />,
},
{
path: '/user-presence',
element: <UserPresenceExample />,
},
]
const router = createBrowserRouter(allExamples)
const rootElement = document.getElementById('root')
const root = createRoot(rootElement!)

File diff suppressed because it is too large Load diff

View file

@ -115,7 +115,7 @@ export function TldrawEditor(props: TldrawEditorProps) {
components?.ErrorFallback === undefined ? DefaultErrorFallback : components?.ErrorFallback
return (
<div ref={setContainer} draggable={false} className="rs-container rs-theme__light" tabIndex={0}>
<div ref={setContainer} draggable={false} className="tl-container tl-theme__light" tabIndex={0}>
<OptionalErrorBoundary
fallback={ErrorFallback ? (error) => <ErrorFallback error={error} /> : null}
onError={(error) => annotateError(error, { tags: { origin: 'react.tldraw-before-app' } })}
@ -312,7 +312,7 @@ export function LoadingScreen({ children }: { children: any }) {
const { Spinner } = useEditorComponents()
return (
<div className="rs-loading">
<div className="tl-loading">
{Spinner ? <Spinner /> : null}
{children}
</div>
@ -321,5 +321,5 @@ export function LoadingScreen({ children }: { children: any }) {
/** @public */
export function ErrorScreen({ children }: { children: any }) {
return <div className="rs-loading">{children}</div>
return <div className="tl-loading">{children}</div>
}

View file

@ -5428,7 +5428,7 @@ export class App extends EventEmitter {
// Get the styles from the container. We'll use these to pull out colors etc.
// NOTE: We can force force a light theme here becasue we don't want export
const fakeContainerEl = document.createElement('div')
fakeContainerEl.className = `rs-container rs-theme__${darkMode ? 'dark' : 'light'}`
fakeContainerEl.className = `tl-container tl-theme__${darkMode ? 'dark' : 'light'}`
document.body.appendChild(fakeContainerEl)
const containerStyle = getComputedStyle(fakeContainerEl)
@ -5554,7 +5554,7 @@ export class App extends EventEmitter {
} else {
// For some reason these styles aren't present in the fake element
// so we need to get them from the real element
font = realContainerStyle.getPropertyValue(`--rs-font-${shape.props.font}`)
font = realContainerStyle.getPropertyValue(`--tl-font-${shape.props.font}`)
fontsUsedInExport.set(shape.props.font, font)
}
}

View file

@ -23,8 +23,8 @@ export class TextManager {
this.app.getContainer().appendChild(elm)
elm.id = `__textMeasure_${uniqueId()}`
elm.classList.add('rs-text')
elm.classList.add('rs-text-measure')
elm.classList.add('tl-text')
elm.classList.add('tl-text-measure')
elm.tabIndex = -1
return elm

View file

@ -576,7 +576,7 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
handlePath =
shape.props.start.type === 'binding' || shape.props.end.type === 'binding' ? (
<path
className="rs-arrow-hint"
className="tl-arrow-hint"
d={info.isStraight ? getStraightArrowHandlePath(info) : getCurvedArrowHandlePath(info)}
strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset}
@ -699,7 +699,7 @@ export class TLArrowUtil extends TLShapeUtil<TLArrowShape> {
{as && <path d={as} />}
{ae && <path d={ae} />}
</g>
<path d={path} className="rs-hitarea-stroke" />
<path d={path} className="tl-hitarea-stroke" />
</SVGContainer>
<ArrowTextLabel
id={shape.id}

View file

@ -27,7 +27,7 @@ export const ArrowTextLabel = React.memo(function ArrowTextLabel({
return (
<div
className="rs-arrow-label"
className="tl-arrow-label"
data-font={font}
data-align={'center'}
data-hastext={!isEmpty}
@ -40,7 +40,7 @@ export const ArrowTextLabel = React.memo(function ArrowTextLabel({
color: labelColor,
}}
>
<div className="rs-arrow-label__inner">
<div className="tl-arrow-label__inner">
<p style={{ width: width ? width : '9px' }}>
{text ? TextHelpers.normalizeTextForDom(text) : ' '}
</p>
@ -48,7 +48,7 @@ export const ArrowTextLabel = React.memo(function ArrowTextLabel({
// Consider replacing with content-editable
<textarea
ref={rInput}
className="rs-text rs-text-input"
className="tl-text tl-text-input"
name="text"
tabIndex={-1}
autoComplete="false"

View file

@ -55,37 +55,37 @@ export class TLBookmarkUtil extends TLBoxUtil<TLBookmarkShape> {
return (
<HTMLContainer>
<div
className="rs-bookmark__container rs-hitarea-stroke"
className="tl-bookmark__container tl-hitarea-stroke"
style={{
boxShadow: rotateBoxShadow(pageRotation, ROTATING_SHADOWS),
}}
>
<div className="rs-bookmark__image_container">
<div className="tl-bookmark__image_container">
{asset?.props.image ? (
<img
className="rs-bookmark__image"
className="tl-bookmark__image"
draggable={false}
src={asset?.props.image}
alt={asset?.props.title || ''}
/>
) : (
<div className="rs-bookmark__placeholder" />
<div className="tl-bookmark__placeholder" />
)}
<HyperlinkButton url={shape.props.url} zoomLevel={this.app.zoomLevel} />
</div>
<div className="rs-bookmark__copy_container">
<div className="tl-bookmark__copy_container">
{asset?.props.title && (
<h2 className="rs-bookmark__heading">
<h2 className="tl-bookmark__heading">
{truncateStringWithEllipsis(asset?.props.title || '', 54)}
</h2>
)}
{asset?.props.description && (
<p className="rs-bookmark__description">
<p className="tl-bookmark__description">
{truncateStringWithEllipsis(asset?.props.description || '', 128)}
</p>
)}
<a
className="rs-bookmark__link"
className="tl-bookmark__link"
href={shape.props.url || ''}
target="_blank"
rel="noopener noreferrer"

View file

@ -110,7 +110,7 @@ export class TLEmbedUtil extends TLBoxUtil<TLEmbedShape> {
const idFromGistUrl = embedInfo.url.split('/').pop()
if (idFromGistUrl) {
return (
<HTMLContainer className="rs-embed-container" id={shape.id}>
<HTMLContainer className="tl-embed-container" id={shape.id}>
<Gist
id={idFromGistUrl}
width={toDomPrecision(w)!}
@ -129,10 +129,10 @@ export class TLEmbedUtil extends TLBoxUtil<TLEmbedShape> {
})
return (
<HTMLContainer className="rs-embed-container" id={shape.id}>
<HTMLContainer className="tl-embed-container" id={shape.id}>
{embedInfo?.definition ? (
<iframe
className={`rs-embed rs-embed-${shape.id}`}
className={`tl-embed tl-embed-${shape.id}`}
sandbox={sandbox}
src={embedInfo.embedUrl}
width={toDomPrecision(w)}
@ -197,7 +197,7 @@ function Gist({
return (
<iframe
ref={rIframe}
className="rs-embed"
className="tl-embed"
draggable={false}
width={toDomPrecision(width)}
height={toDomPrecision(height)}

View file

@ -33,9 +33,9 @@ export class TLFrameUtil extends TLBoxUtil<TLFrameShape> {
return (
<>
<SVGContainer>
<rect className="rs-hitarea-stroke" width={bounds.width} height={bounds.height} />
<rect className="tl-hitarea-stroke" width={bounds.width} height={bounds.height} />
<rect
className="rs-frame__body"
className="tl-frame__body"
width={bounds.width}
height={bounds.height}
fill="none"
@ -164,7 +164,7 @@ export class TLFrameUtil extends TLBoxUtil<TLFrameShape> {
<rect
width={toDomPrecision(bounds.width)}
height={toDomPrecision(bounds.height)}
className={`rs-frame-indicator`}
className={`tl-frame-indicator`}
/>
)
}

View file

@ -67,17 +67,17 @@ export const FrameHeading = function FrameHeading({
return (
<div
className="rs-frame-heading"
className="tl-frame-heading"
style={{
overflow: isEditing ? 'visible' : 'hidden',
maxWidth: `calc(var(--rs-zoom) * ${
maxWidth: `calc(var(--tl-zoom) * ${
labelSide === 'top' || labelSide === 'bottom' ? Math.ceil(width) : Math.ceil(height)
}px + var(--space-5))`,
bottom: Math.ceil(height),
transform: `${labelTranslate} scale(var(--rs-scale)) translateX(calc(-1 * var(--space-3))`,
transform: `${labelTranslate} scale(var(--tl-scale)) translateX(calc(-1 * var(--space-3))`,
}}
>
<div className="rs-frame-heading-hit-area">
<div className="tl-frame-heading-hit-area">
<FrameLabelInput ref={rInput} id={id} name={name} isEditing={isEditing} />
</div>
</div>

View file

@ -69,9 +69,9 @@ export const FrameLabelInput = forwardRef<
)
return (
<div className={`rs-frame-label ${isEditing ? 'rs-frame-label__editing' : ''}`}>
<div className={`tl-frame-label ${isEditing ? 'tl-frame-label__editing' : ''}`}>
<input
className="rs-frame-name-input"
className="tl-frame-name-input"
ref={ref}
style={{ display: isEditing ? undefined : 'none' }}
value={name}

View file

@ -26,7 +26,7 @@ export const DashStylePolygon = React.memo(function DashStylePolygon({
lines.map((l, i) => (
<path
key={`line_bg_${i}`}
className={'rs-hitarea-stroke'}
className={'tl-hitarea-stroke'}
fill="none"
d={`M${l[0].x},${l[0].y}L${l[1].x},${l[1].y}`}
/>

View file

@ -75,7 +75,7 @@ export class TLGroupUtil extends TLShapeUtil<TLGroupShape> {
return (
<SVGContainer id={shape.id}>
<DashedOutlineBox className="rs-group" bounds={bounds} zoomLevel={zoomLevel} />
<DashedOutlineBox className="tl-group" bounds={bounds} zoomLevel={zoomLevel} />
</SVGContainer>
)
}

View file

@ -125,26 +125,26 @@ export class TLImageUtil extends TLBoxUtil<TLImageShape> {
{asset?.props.src && showCropPreview && (
<div style={containerStyle}>
<div
className={`rs-image rs-image-${shape.id}-crop`}
className={`tl-image tl-image-${shape.id}-crop`}
style={{
opacity: 0.1,
backgroundImage: `url(${
!shape.props.playing || reduceMotion ? staticFrameSrc : asset.props.src
}`,
})`,
}}
draggable={false}
/>
</div>
)}
<HTMLContainer id={shape.id} style={{ overflow: 'hidden' }}>
<div className="rs-image-container" style={containerStyle}>
<div className="tl-image-container" style={containerStyle}>
{asset?.props.src ? (
<div
className={`rs-image rs-image-${shape.id}`}
className={`tl-image tl-image-${shape.id}`}
style={{
backgroundImage: `url(${
!shape.props.playing || reduceMotion ? staticFrameSrc : asset.props.src
}`,
})`,
}}
draggable={false}
/>
@ -154,7 +154,7 @@ export class TLImageUtil extends TLBoxUtil<TLImageShape> {
</g>
) : null}
{asset?.props.isAnimated && !shape.props.playing && (
<div className="rs-image__meda-tag">GIF</div>
<div className="tl-image__tg">GIF</div>
)}
</div>
</HTMLContainer>

View file

@ -69,13 +69,13 @@ export class TLNoteUtil extends TLShapeUtil<TLNoteShape> {
}}
>
<div
className="rs-note__container rs-hitarea-fill"
className="tl-note__container tl-hitarea-fill"
style={{
color: `var(--palette-${adjustedColor})`,
backgroundColor: `var(--palette-${adjustedColor})`,
}}
>
<div className="rs-note__scrim" />
<div className="tl-note__scrim" />
<TextLabel
id={id}
type={type}

View file

@ -101,7 +101,7 @@ export class TLTextUtil extends TLShapeUtil<TLTextShape> {
return (
<HTMLContainer id={shape.id}>
<div
className="rs-text-shape__wrapper rs-text-shadow"
className="tl-text-shape__wrapper tl-text-shadow"
data-font={shape.props.font}
data-align={shape.props.align}
data-hastext={!isEmpty}
@ -116,13 +116,13 @@ export class TLTextUtil extends TLShapeUtil<TLTextShape> {
height: Math.max(FONT_SIZES[shape.props.size] * TEXT_PROPS.lineHeight, height),
}}
>
<div className="rs-text rs-text-content" dir="ltr">
<div className="tl-text tl-text-content" dir="ltr">
{text}
</div>
{isEditing || isEditableFromHover ? (
<textarea
ref={rInput}
className="rs-text rs-text-input"
className="tl-text tl-text-input"
name="text"
tabIndex={-1}
autoComplete="false"

View file

@ -60,7 +60,7 @@ export const TLVideoShapeDef = defineShape<TLVideoShape, TLVideoUtil>({
// Function from v1, could be improved bu explicitly using this.model.time (?)
function serializeVideo(id: string): string {
const splitId = id.split(':')[1]
const video = document.querySelector(`.rs-video-shape-${splitId}`) as HTMLVideoElement
const video = document.querySelector(`.tl-video-shape-${splitId}`) as HTMLVideoElement
if (video) {
const canvas = document.createElement('canvas')
canvas.width = video.videoWidth
@ -179,11 +179,11 @@ const TLVideoUtilComponent = track(function TLVideoUtilComponent(props: {
return (
<>
<HTMLContainer id={shape.id}>
<div className="rs-counter-scaled">
<div className="tl-counter-scaled">
{asset?.props.src ? (
<video
ref={rVideo}
className={`rs-video rs-video-shape-${shape.id.split(':')[1]} rs-hitarea-stroke`}
className={`tl-video tl-video-shape-${shape.id.split(':')[1]} tl-hitarea-stroke`}
width="100%"
height="100%"
draggable={false}

View file

@ -1,3 +1,4 @@
import classNames from 'classnames'
import { stopEventPropagation } from '../../../utils/dom'
const LINK_ICON =
@ -6,7 +7,9 @@ const LINK_ICON =
export function HyperlinkButton({ url, zoomLevel }: { url: string; zoomLevel: number }) {
return (
<a
className={`rs-hyperlink-button ${zoomLevel < 0.5 ? 'hidden' : ''}`}
className={classNames('tl-hyperlink-button', {
'tl-hyperlink-button__hidden': zoomLevel < 0.5,
})}
href={url}
target="_blank"
rel="noopener noreferrer"
@ -16,7 +19,7 @@ export function HyperlinkButton({ url, zoomLevel }: { url: string; zoomLevel: nu
draggable={false}
>
<div
className="rs-hyperlink-button__icon"
className="tl-hyperlink-button__icon"
style={{
mask: `url("${LINK_ICON}") center 100% / 100% no-repeat`,
WebkitMask: `url("${LINK_ICON}") center 100% / 100% no-repeat`,

View file

@ -14,15 +14,15 @@ export interface ShapeFillProps {
export const ShapeFill = React.memo(function ShapeFill({ d, color, fill }: ShapeFillProps) {
switch (fill) {
case 'none': {
return <path className={'rs-hitarea-stroke'} fill="none" d={d} />
return <path className={'tl-hitarea-stroke'} fill="none" d={d} />
}
case 'solid': {
return (
<path className={'rs-hitarea-fill-solid'} fill={`var(--palette-${color}-semi)`} d={d} />
<path className={'tl-hitarea-fill-solid'} fill={`var(--palette-${color}-semi)`} d={d} />
)
}
case 'semi': {
return <path className={'rs-hitarea-fill-solid'} fill={`var(--palette-solid)`} d={d} />
return <path className={'tl-hitarea-fill-solid'} fill={`var(--palette-solid)`} d={d} />
}
case 'pattern': {
return <PatternFill color={color} fill={fill} d={d} />
@ -40,7 +40,7 @@ const PatternFill = function PatternFill({ d, color }: ShapeFillProps) {
return (
<>
<path className={'rs-hitarea-fill-solid'} fill={`var(--palette-${color}-pattern)`} d={d} />
<path className={'tl-hitarea-fill-solid'} fill={`var(--palette-${color}-pattern)`} d={d} />
<path
fill={
teenyTiny

View file

@ -42,7 +42,7 @@ export const TextLabel = React.memo(function TextLabel<
return (
<div
className="rs-text-label"
className="tl-text-label"
data-font={font}
data-align={align}
data-hastext={!isEmpty}
@ -50,7 +50,7 @@ export const TextLabel = React.memo(function TextLabel<
data-textwrap={!!wrap}
>
<div
className="rs-text-label__inner"
className="tl-text-label__inner"
style={{
fontSize: LABEL_FONT_SIZES[size],
lineHeight: LABEL_FONT_SIZES[size] * TEXT_PROPS.lineHeight + 'px',
@ -59,14 +59,14 @@ export const TextLabel = React.memo(function TextLabel<
color: labelColor,
}}
>
<div className="rs-text rs-text-content" dir="ltr">
<div className="tl-text tl-text-content" dir="ltr">
{TextHelpers.normalizeTextForDom(text)}
</div>
{isInteractive ? (
// Consider replacing with content-editable
<textarea
ref={rInput}
className="rs-text rs-text-input"
className="tl-text tl-text-input"
name="text"
tabIndex={-1}
autoComplete="false"

View file

@ -37,7 +37,6 @@ export const Canvas = track(function Canvas({
const rCanvas = React.useRef<HTMLDivElement>(null)
const rHtmlLayer = React.useRef<HTMLDivElement>(null)
const rSvgLayer = React.useRef<SVGGElement>(null)
useScreenBounds()
useDocumentEvents()
@ -49,9 +48,8 @@ export const Canvas = track(function Canvas({
useQuickReactor(
'position layers',
() => {
const svgElm = rSvgLayer.current
const htmlElm = rHtmlLayer.current
if (!(svgElm && htmlElm)) return
if (!htmlElm) return
const { x, y, z } = app.camera
@ -61,10 +59,6 @@ export const Canvas = track(function Canvas({
const offset =
z >= 1 ? modulate(z, [1, 8], [0.125, 0.5], true) : modulate(z, [0.1, 1], [-2, 0.125], true)
svgElm.style.setProperty(
'transform',
`scale(${toDomPrecision(z)}) translate(${toDomPrecision(x)}px,${toDomPrecision(y)}px)`
)
htmlElm.style.setProperty(
'transform',
`scale(${toDomPrecision(z)}) translate(${toDomPrecision(x + offset)}px,${toDomPrecision(
@ -84,18 +78,15 @@ export const Canvas = track(function Canvas({
React.useEffect(() => {
if (patternIsReady && app.isSafari) {
const svgElm = rSvgLayer.current
const htmlElm = rHtmlLayer.current
if (svgElm && htmlElm) {
if (htmlElm) {
// Wait for `patternContext` to be picked up
requestAnimationFrame(() => {
svgElm.style.display = 'none'
htmlElm.style.display = 'none'
// Wait for 'display = "none"' to take effect
requestAnimationFrame(() => {
svgElm.style.display = ''
htmlElm.style.display = ''
})
})
@ -108,24 +99,26 @@ export const Canvas = track(function Canvas({
}, [])
return (
<div ref={rCanvas} draggable={false} className="rs-canvas" data-wd="canvas" {...events}>
<div ref={rCanvas} draggable={false} className="tl-canvas" data-wd="canvas" {...events}>
{Background && <Background />}
<GridWrapper />
<UiLogger />
<div ref={rHtmlLayer} className="rs-html-layer" draggable={false}>
<div ref={rHtmlLayer} className="tl-html-layer" draggable={false}>
<svg className="tl-svg-context">
<defs>
{patternContext}
{Cursor && <Cursor />}
<CollaboratorHint />
<ArrowheadDot />
<ArrowheadCross />
{SvgDefs && <SvgDefs />}
</defs>
</svg>
<SelectionBg />
<ShapesToDisplay />
</div>
<svg className="rs-svg-layer">
{patternContext}
<defs>
{Cursor && <Cursor />}
<CollaboratorHint />
<ArrowheadDot />
<ArrowheadCross />
{SvgDefs && <SvgDefs />}
</defs>
<g ref={rSvgLayer}>
<div className="tl-shapes">
<ShapesToDisplay />
</div>
<div className="tl-overlays">
<ScribbleWrapper />
<BrushWrapper />
<ZoomBrushWrapper />
@ -140,8 +133,8 @@ export const Canvas = track(function Canvas({
) : (
<LiveCollaborators />
)}
</g>
</svg>
</div>
</div>
</div>
)
})
@ -255,11 +248,13 @@ const HandlesWrapper = track(function HandlesWrapper() {
handlesToDisplay.sort((a) => (a.type === 'vertex' ? 1 : -1))
return (
<g transform={Matrix2d.toCssString(transform)}>
{handlesToDisplay.map((handle) => {
return <HandleWrapper key={handle.id} shapeId={onlySelectedShape.id} handle={handle} />
})}
</g>
<svg className="tl-svg-origin-container">
<g transform={Matrix2d.toCssString(transform)}>
{handlesToDisplay.map((handle) => {
return <HandleWrapper key={handle.id} shapeId={onlySelectedShape.id} handle={handle} />
})}
</g>
</svg>
)
})
@ -320,11 +315,11 @@ const SelectedIdIndicators = track(function SelectedIdIndicators() {
if (!shouldDisplay) return null
return (
<g>
<>
{app.selectedIds.map((id) => (
<ShapeIndicator key={id + '_indicator'} id={id} />
))}
</g>
</>
)
})
@ -350,11 +345,11 @@ const HintedShapeIndicator = track(function HintedShapeIndicator() {
if (!ids.length) return null
return (
<g aria-label="HINTED SHAPES">
<>
{ids.map((id) => (
<ShapeIndicator id={id} key={id + '_hinting'} isHinting />
))}
</g>
</>
)
})
@ -383,7 +378,7 @@ function CollaboratorHint() {
function ArrowheadDot() {
return (
<marker id="arrowhead-dot" className="rs-arrow-hint" refX="3.0" refY="3.0" orient="0">
<marker id="arrowhead-dot" className="tl-arrow-hint" refX="3.0" refY="3.0" orient="0">
<circle cx="3" cy="3" r="2" strokeDasharray="100%" />
</marker>
)
@ -391,7 +386,7 @@ function ArrowheadDot() {
function ArrowheadCross() {
return (
<marker id="arrowhead-cross" className="rs-arrow-hint" refX="3.0" refY="3.0" orient="auto">
<marker id="arrowhead-cross" className="tl-arrow-hint" refX="3.0" refY="3.0" orient="auto">
<line x1="1.5" y1="1.5" x2="4.5" y2="4.5" strokeDasharray="100%" />
<line x1="1.5" y1="4.5" x2="4.5" y2="1.5" strokeDasharray="100%" />
</marker>

View file

@ -11,11 +11,12 @@ interface CropHandlesProps {
export function CropHandles({ size, width, height, hideAlternateHandles }: CropHandlesProps) {
const cropStrokeWidth = toDomPrecision(size / 3)
const offset = cropStrokeWidth / 2
return (
<>
<svg className="tl-svg-origin-container">
{/* Top left */}
<polyline
className="rs-corner-crop-handle"
className="tl-corner-crop-handle"
points={`
${toDomPrecision(0 - offset)},${toDomPrecision(size)}
${toDomPrecision(0 - offset)},${toDomPrecision(0 - offset)}
@ -26,8 +27,8 @@ export function CropHandles({ size, width, height, hideAlternateHandles }: CropH
/>
{/* Top */}
<line
className={classNames('rs-corner-crop-edge-handle', {
'rs-hidden': hideAlternateHandles,
className={classNames('tl-corner-crop-edge-handle', {
'tl-hidden': hideAlternateHandles,
})}
x1={toDomPrecision(width / 2 - size)}
y1={toDomPrecision(0 - offset)}
@ -39,8 +40,8 @@ export function CropHandles({ size, width, height, hideAlternateHandles }: CropH
/>
{/* Top right */}
<polyline
className={classNames('rs-corner-crop-handle', {
'rs-hidden': hideAlternateHandles,
className={classNames('tl-corner-crop-handle', {
'tl-hidden': hideAlternateHandles,
})}
points={`
${toDomPrecision(width - size)},${toDomPrecision(0 - offset)}
@ -52,8 +53,8 @@ export function CropHandles({ size, width, height, hideAlternateHandles }: CropH
/>
{/* Right */}
<line
className={classNames('rs-corner-crop-edge-handle', {
'rs-hidden': hideAlternateHandles,
className={classNames('tl-corner-crop-edge-handle', {
'tl-hidden': hideAlternateHandles,
})}
x1={toDomPrecision(width + offset)}
y1={toDomPrecision(height / 2 - size)}
@ -65,7 +66,7 @@ export function CropHandles({ size, width, height, hideAlternateHandles }: CropH
/>
{/* Bottom right */}
<polyline
className="rs-corner-crop-handle"
className="tl-corner-crop-handle"
points={`
${toDomPrecision(width + offset)},${toDomPrecision(height - size)}
${toDomPrecision(width + offset)},${toDomPrecision(height + offset)}
@ -76,8 +77,8 @@ export function CropHandles({ size, width, height, hideAlternateHandles }: CropH
/>
{/* Bottom */}
<line
className={classNames('rs-corner-crop-edge-handle', {
'rs-hidden': hideAlternateHandles,
className={classNames('tl-corner-crop-edge-handle', {
'tl-hidden': hideAlternateHandles,
})}
x1={toDomPrecision(width / 2 - size)}
y1={toDomPrecision(height + offset)}
@ -89,8 +90,8 @@ export function CropHandles({ size, width, height, hideAlternateHandles }: CropH
/>
{/* Bottom left */}
<polyline
className={classNames('rs-corner-crop-handle', {
'rs-hidden': hideAlternateHandles,
className={classNames('tl-corner-crop-handle', {
'tl-hidden': hideAlternateHandles,
})}
points={`
${toDomPrecision(0 + size)},${toDomPrecision(height + offset)}
@ -102,8 +103,8 @@ export function CropHandles({ size, width, height, hideAlternateHandles }: CropH
/>
{/* Left */}
<line
className={classNames('rs-corner-crop-edge-handle', {
'rs-hidden': hideAlternateHandles,
className={classNames('tl-corner-crop-edge-handle', {
'tl-hidden': hideAlternateHandles,
})}
x1={toDomPrecision(0 - offset)}
y1={toDomPrecision(height / 2 - size)}
@ -113,6 +114,6 @@ export function CropHandles({ size, width, height, hideAlternateHandles }: CropH
data-wd="selection.crop.left"
aria-label="left handle"
/>
</>
</svg>
)
}

View file

@ -2,5 +2,5 @@
export type TLBackgroundComponent = () => JSX.Element | null
export function DefaultBackground() {
return <div className="rs-background" />
return <div className="tl-background" />
}

View file

@ -1,32 +1,40 @@
import { toDomPrecision } from '@tldraw/primitives'
import { Box2dModel } from '@tldraw/tlschema'
import { useRef } from 'react'
import { useTransform } from '../hooks/useTransform'
/** @public */
export type TLBrushComponent = (props: { brush: Box2dModel; color?: string }) => any | null
export const DefaultBrush: TLBrushComponent = ({ brush, color }) => {
return color ? (
<g className="rs-brush" transform={`translate(${brush.x},${brush.y})`}>
<rect
width={toDomPrecision(Math.max(1, brush.w))}
height={toDomPrecision(Math.max(1, brush.h))}
fill={color}
opacity={0.1}
/>
<rect
width={toDomPrecision(Math.max(1, brush.w))}
height={toDomPrecision(Math.max(1, brush.h))}
fill="none"
stroke={color}
opacity={0.1}
/>
</g>
) : (
<rect
className="rs-brush rs-brush__default"
width={toDomPrecision(Math.max(1, brush.w))}
height={toDomPrecision(Math.max(1, brush.h))}
transform={`translate(${brush.x},${brush.y})`}
/>
const rSvg = useRef<SVGSVGElement>(null)
useTransform(rSvg, brush.x, brush.y)
return (
<svg className="tl-svg-origin-container" ref={rSvg}>
{color ? (
<g className="tl-brush">
<rect
width={toDomPrecision(Math.max(1, brush.w))}
height={toDomPrecision(Math.max(1, brush.h))}
fill={color}
opacity={0.75}
/>
<rect
width={toDomPrecision(Math.max(1, brush.w))}
height={toDomPrecision(Math.max(1, brush.h))}
fill="none"
stroke={color}
opacity={0.1}
/>
</g>
) : (
<rect
className="tl-brush tl-brush__default"
width={toDomPrecision(Math.max(1, brush.w))}
height={toDomPrecision(Math.max(1, brush.h))}
/>
)}
</svg>
)
}

View file

@ -1,5 +1,7 @@
import { Box2d, clamp, radiansToDegrees, Vec2d } from '@tldraw/primitives'
import { Vec2dModel } from '@tldraw/tlschema'
import { useRef } from 'react'
import { useTransform } from '../hooks/useTransform'
export type TLCollaboratorHintComponent = (props: {
point: Vec2dModel
@ -14,18 +16,19 @@ export const DefaultCollaboratorHint: TLCollaboratorHintComponent = ({
color,
viewport,
}) => {
const x = clamp(point.x, viewport.minX + 5 / zoom, viewport.maxX - 5 / zoom)
const y = clamp(point.y, viewport.minY + 5 / zoom, viewport.maxY - 5 / zoom)
const rSvg = useRef<SVGSVGElement>(null)
const direction = radiansToDegrees(Vec2d.Angle(viewport.center, point))
useTransform(
rSvg,
clamp(point.x, viewport.minX + 5 / zoom, viewport.maxX - 5 / zoom),
clamp(point.y, viewport.minY + 5 / zoom, viewport.maxY - 5 / zoom),
1 / zoom,
radiansToDegrees(Vec2d.Angle(viewport.center, point))
)
return (
<>
<use
href="#cursor_hint"
transform={`translate(${x}, ${y}) scale(${1 / zoom}) rotate(${direction})`}
color={color}
/>
</>
<svg ref={rSvg} className="tl-svg-origin-container">
<use href="#cursor_hint" color={color} />
</svg>
)
}

View file

@ -1,42 +1,31 @@
import { Vec2dModel } from '@tldraw/tlschema'
import { memo } from 'react'
import { truncateStringWithEllipsis } from '../utils/dom'
/** @public */
export type TLCursorComponent = (props: {
point: Vec2dModel | null
zoom: number
color?: string
nameTag: string | null
name: string | null
}) => any | null
const _Cursor: TLCursorComponent = ({ zoom, point, color, nameTag }) => {
const _Cursor: TLCursorComponent = ({ zoom, point, color, name }) => {
if (!point) return null
return (
<g transform={`translate(${point.x}, ${point.y}) scale(${1 / zoom})`}>
<use href="#cursor" color={color} />
{nameTag !== null && nameTag !== '' && (
<foreignObject
x="13"
y="16"
width="0"
height="0"
style={{
overflow: 'visible',
}}
>
<div
className="rs-nametag"
style={{
backgroundColor: color,
}}
>
{truncateStringWithEllipsis(nameTag, 40)}
</div>
</foreignObject>
<div
className="tl-cursor"
style={{ transform: `translate(${point.x}px, ${point.y}px) scale(${1 / zoom})` }}
>
<svg>
<use href="#cursor" color={color} />
</svg>
{name !== null && name !== '' && (
<div className="tl-nametag" style={{ backgroundColor: color }}>
{name}
</div>
)}
</g>
</div>
)
}

View file

@ -53,8 +53,8 @@ export const DefaultErrorFallback: TLErrorFallback = ({ error, app }) => {
let foundParentThemeClass = false
while (parent) {
if (
parent.classList.contains('rs-theme__dark') ||
parent.classList.contains('rs-theme__light')
parent.classList.contains('tl-theme__dark') ||
parent.classList.contains('tl-theme__light')
) {
foundParentThemeClass = true
break
@ -68,7 +68,7 @@ export const DefaultErrorFallback: TLErrorFallback = ({ error, app }) => {
// if we can't find a theme class from the app or from a parent, we have
// to fall back on using a media query:
setIsDarkMode(window.matchMedia('(prefers-color-scheme: dark)').matches)
setIsDarkMode(window.matchMedia('(prefetl-color-scheme: dark)').matches)
}, [isDarkModeFromApp])
useEffect(() => {
@ -116,14 +116,14 @@ My browser: ${navigator.userAgent}`
<div
ref={containerRef}
className={classNames(
'rs-container rs-error-boundary',
'tl-container tl-error-boundary',
// error-boundary is sometimes used outside of the theme
// container, so we need to provide it with a theme for our
// styles to work correctly
isDarkMode === null ? '' : isDarkMode ? 'rs-theme__dark' : 'rs-theme__light'
isDarkMode === null ? '' : isDarkMode ? 'tl-theme__dark' : 'tl-theme__light'
)}
>
<div className="rs-error-boundary__overlay" />
<div className="tl-error-boundary__overlay" />
{app && (
// opportunistically attempt to render the canvas to reassure
// the user that their document is still there. there's a good
@ -133,24 +133,24 @@ My browser: ${navigator.userAgent}`
// a plain grey background.
<ErrorBoundary onError={noop} fallback={() => null}>
<AppContext.Provider value={app}>
<div className="rs-overlay rs-error-boundary__canvas">
<div className="tl-overlay tl-error-boundary__canvas">
<Canvas />
</div>
</AppContext.Provider>
</ErrorBoundary>
)}
<div
className={classNames('rs-modal', 'rs-error-boundary__content', {
'rs-error-boundary__content__expanded': shouldShowError && !shouldShowResetConfirmation,
className={classNames('tl-modal', 'tl-error-boundary__content', {
'tl-error-boundary__content__expanded': shouldShowError && !shouldShowResetConfirmation,
})}
>
{shouldShowResetConfirmation ? (
<>
<h2>Are you sure?</h2>
<p>Resetting your data will delete your drawing and cannot be undone.</p>
<div className="rs-error-boundary__content__actions">
<div className="tl-error-boundary__content__actions">
<button onClick={() => setShouldShowResetConfirmation(false)}>Cancel</button>
<button className="rs-error-boundary__reset" onClick={resetLocalState}>
<button className="tl-error-boundary__reset" onClick={resetLocalState}>
Reset data
</button>
</div>
@ -164,25 +164,25 @@ My browser: ${navigator.userAgent}`
<a href="https://discord.gg/Cq6cPsTfNy">ask for help on Discord</a>.
</p>
{shouldShowError && (
<div className="rs-error-boundary__content__error">
<div className="tl-error-boundary__content__error">
<pre>
<code>{errorStack ?? errorMessage}</code>
</pre>
<button onClick={copyError}>{didCopy ? 'Copied!' : 'Copy'}</button>
</div>
)}
<div className="rs-error-boundary__content__actions">
<div className="tl-error-boundary__content__actions">
<button onClick={() => setShouldShowError(!shouldShowError)}>
{shouldShowError ? 'Hide details' : 'Show details'}
</button>
<div className="rs-error-boundary__content__actions__group">
<div className="tl-error-boundary__content__actions__group">
<button
className="rs-error-boundary__reset"
className="tl-error-boundary__reset"
onClick={() => setShouldShowResetConfirmation(true)}
>
Reset data
</button>
<button className="rs-error-boundary__refresh" onClick={refresh}>
<button className="tl-error-boundary__refresh" onClick={refresh}>
Refresh Page
</button>
</div>

View file

@ -5,9 +5,9 @@ export type TLHandleComponent = (props: { shapeId: TLShapeId; handle: TLHandle }
export const DefaultHandle: TLHandleComponent = ({ handle }) => {
return (
<g className={classNames('rs-handle', { 'rs-handle-hint': handle.type !== 'vertex' })}>
<circle className="rs-handle-bg" />
<circle className="rs-handle-fg" />
<g className={classNames('tl-handle', { 'tl-handle__hint': handle.type !== 'vertex' })}>
<circle className="tl-handle__bg" />
<circle className="tl-handle__fg" />
</g>
)
}

View file

@ -21,11 +21,13 @@ export const DefaultScribble: TLScribbleComponent = ({ scribble, zoom, color, op
)
return (
<path
className="tl-scribble"
d={d}
fill={color ?? `var(--color-${scribble.color})`}
opacity={opacity ?? scribble.opacity}
/>
<svg className="tl-svg-origin-container">
<path
className="tl-scribble"
d={d}
fill={color ?? `var(--color-${scribble.color})`}
opacity={opacity ?? scribble.opacity}
/>
</svg>
)
}

View file

@ -3,5 +3,5 @@ export type TLShapeErrorFallback = (props: { error: unknown }) => any | null
/** @internal */
export const DefaultShapeErrorFallback: TLShapeErrorFallback = () => {
return <div className="rs-shape-error-boundary" />
return <div className="tl-shape-error-boundary" />
}

View file

@ -25,12 +25,12 @@ function PointsSnapLine({ points, zoom }: { zoom: number } & PointsSnapLine) {
}
return (
<g className="rs-snap-line">
<g className="tl-snap-line">
<line x1={firstX} y1={firstY} x2={secondX} y2={secondY} />
{points.map((p, i) => (
<g transform={`translate(${p.x},${p.y})`} key={i}>
<path
className="rs-snap-point"
className="tl-snap-point"
d={`M ${-l},${-l} L ${l},${l} M ${-l},${l} L ${l},${-l}`}
/>
</g>
@ -84,7 +84,7 @@ function GapsSnapLine({ gaps, direction, zoom }: { zoom: number } & GapsSnapLine
const midPoint = (edgeIntersection[0] + edgeIntersection[1]) / 2
return (
<g className="rs-snap-line">
<g className="tl-snap-line">
{gaps.map(({ startEdge, endEdge }, i) => (
<React.Fragment key={i}>
{horizontal ? (
@ -151,12 +151,13 @@ function GapsSnapLine({ gaps, direction, zoom }: { zoom: number } & GapsSnapLine
export type TLSnapLineComponent = (props: { line: SnapLine; zoom: number }) => any
export const DefaultSnapLine: TLSnapLineComponent = ({ line, zoom }) => {
switch (line.type) {
case 'points':
return <PointsSnapLine {...line} zoom={zoom} />
case 'gaps':
return <GapsSnapLine {...line} zoom={zoom} />
default:
return null
}
return (
<svg className="tl-svg-origin-container">
{line.type === 'points' ? (
<PointsSnapLine {...line} zoom={zoom} />
) : line.type === 'gaps' ? (
<GapsSnapLine {...line} zoom={zoom} />
) : null}
</svg>
)
}

View file

@ -6,7 +6,7 @@ export type HTMLContainerProps = React.HTMLAttributes<HTMLDivElement>
/** @public */
export function HTMLContainer({ children, className = '', ...rest }: HTMLContainerProps) {
return (
<div {...rest} className={`rs-html-container ${className}`}>
<div {...rest} className={`tl-html-container ${className}`}>
{children}
</div>
)

View file

@ -30,7 +30,7 @@ export const LiveCollaborators = track(function Collaborators() {
const presences = useActivePresences()
return (
<g>
<>
{presences.value.map((userPresence) => (
<Collaborator
key={userPresence.id}
@ -39,7 +39,7 @@ export const LiveCollaborators = track(function Collaborators() {
zoom={zoomLevel}
/>
))}
</g>
</>
)
})
@ -119,7 +119,7 @@ const Collaborator = track(function Collaborator({
point={cursor}
color={color}
zoom={zoom}
nameTag={name !== 'New User' ? name : null}
name={name !== 'New User' ? name : null}
/>
) : CollaboratorHint ? (
<CollaboratorHint
@ -136,6 +136,7 @@ const Collaborator = track(function Collaborator({
scribble={scribble}
color={color}
zoom={zoom}
opacity={0.1}
/>
) : null}
{CollaboratorShapeIndicator &&

View file

@ -8,11 +8,11 @@ import { usePresence } from '../hooks/usePresence'
export const LiveCollaboratorsNext = track(function Collaborators() {
const peerIds = usePeerIds()
return (
<g>
<>
{peerIds.map((id) => (
<Collaborator key={id} userId={id} />
))}
</g>
</>
)
})
@ -56,7 +56,7 @@ const Collaborator = track(function Collaborator({ userId }: { userId: TLUserId
point={cursor}
color={color}
zoom={zoomLevel}
nameTag={userName !== 'New User' ? userName : null}
name={userName !== 'New User' ? userName : null}
/>
) : CollaboratorHint ? (
<CollaboratorHint

View file

@ -6,7 +6,7 @@ export type SVGContainerProps = React.HTMLAttributes<SVGElement>
/** @public */
export function SVGContainer({ children, className = '', ...rest }: SVGContainerProps) {
return (
<svg {...rest} className={`rs-svg-container ${className}`}>
<svg {...rest} className={`tl-svg-container ${className}`}>
{children}
</svg>
)

View file

@ -1,13 +1,16 @@
import { Matrix2d, RotateCorner, toDomPrecision } from '@tldraw/primitives'
import { RotateCorner, toDomPrecision } from '@tldraw/primitives'
import classNames from 'classnames'
import { useRef } from 'react'
import { track } from 'signia-react'
import { useApp } from '../hooks/useApp'
import { getCursor } from '../hooks/useCursor'
import { useSelectionEvents } from '../hooks/useSelectionEvents'
import { useTransform } from '../hooks/useTransform'
import { CropHandles } from './CropHandles'
export const SelectionFg = track(function SelectionFg() {
const app = useApp()
const rSvg = useRef<SVGSVGElement>(null)
const isReadonlyMode = app.isReadOnly
const topEvents = useSelectionEvents('top')
@ -24,6 +27,8 @@ export const SelectionFg = track(function SelectionFg() {
const bounds = app.selectionBounds
useTransform(rSvg, bounds?.x, bounds?.y, 1, app.selectionRotation)
if (!bounds) return null
const zoom = app.zoomLevel
@ -49,13 +54,6 @@ export const SelectionFg = track(function SelectionFg() {
const targetSizeX = (isSmallX ? targetSize / 2 : targetSize) * (mobileHandleMultiplier * 0.75)
const targetSizeY = (isSmallY ? targetSize / 2 : targetSize) * (mobileHandleMultiplier * 0.75)
const transform = Matrix2d.toCssString(
Matrix2d.Compose(
Matrix2d.Translate(bounds.minX, bounds.minY),
Matrix2d.Rotate(app.selectionRotation)
)
)
const onlyShape = shapes.length === 1 ? shapes[0] : null
const showSelectionBounds =
@ -156,254 +154,256 @@ export const SelectionFg = track(function SelectionFg() {
textHandleHeight * zoom >= 4
return (
<g data-wd="selection-foreground" className="tlui-selection__fg" transform={transform}>
<rect
className={classNames('tlui-selection__fg-outline', { 'rs-hidden': !shouldDisplayBox })}
width={toDomPrecision(width)}
height={toDomPrecision(height)}
/>
<RotateCornerHandle
data-wd="selection.rotate.top-left"
cx={0}
cy={0}
targetSize={targetSize}
corner="top_left_rotate"
cursor={isDefaultCursor ? getCursor('nwse-rotate', rotation) : undefined}
isHidden={hideRotateCornerHandles}
/>
<RotateCornerHandle
data-wd="selection.rotate.top-right"
cx={width + targetSize * 3}
cy={0}
targetSize={targetSize}
corner="top_right_rotate"
cursor={isDefaultCursor ? getCursor('nesw-rotate', rotation) : undefined}
isHidden={hideRotateCornerHandles}
/>
<RotateCornerHandle
data-wd="selection.rotate.bottom-left"
cx={0}
cy={height + targetSize * 3}
targetSize={targetSize}
corner="bottom_left_rotate"
cursor={isDefaultCursor ? getCursor('swne-rotate', rotation) : undefined}
isHidden={hideRotateCornerHandles}
/>
<RotateCornerHandle
data-wd="selection.rotate.bottom-right"
cx={width + targetSize * 3}
cy={height + targetSize * 3}
targetSize={targetSize}
corner="bottom_right_rotate"
cursor={isDefaultCursor ? getCursor('senw-rotate', rotation) : undefined}
isHidden={hideRotateCornerHandles}
/>{' '}
<MobileRotateHandle
data-wd="selection.rotate.mobile"
cx={isSmallX ? -targetSize * 1.5 : width / 2}
cy={isSmallX ? height / 2 : -targetSize * 1.5}
size={size}
isHidden={hideMobileRotateHandle}
/>
{/* Targets */}
<rect
className={classNames('rs-transparent', {
'rs-hidden': hideEdgeTargets,
})}
data-wd="selection.resize.top"
aria-label="top target"
pointerEvents="all"
x={0}
y={toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY))}
width={toDomPrecision(Math.max(1, width))}
height={toDomPrecision(Math.max(1, targetSizeY * 2))}
style={isDefaultCursor ? { cursor: getCursor('ns-resize', rotation) } : undefined}
{...topEvents}
/>
<rect
className={classNames('rs-transparent', {
'rs-hidden': hideEdgeTargets,
})}
data-wd="selection.resize.right"
aria-label="right target"
pointerEvents="all"
x={toDomPrecision(width - (isSmallX ? 0 : targetSizeX))}
y={0}
height={toDomPrecision(Math.max(1, height))}
width={toDomPrecision(Math.max(1, targetSizeX * 2))}
style={isDefaultCursor ? { cursor: getCursor('ew-resize', rotation) } : undefined}
{...rightEvents}
/>
<rect
className={classNames('rs-transparent', {
'rs-hidden': hideEdgeTargets,
})}
data-wd="selection.resize.bottom"
aria-label="bottom target"
pointerEvents="all"
x={0}
y={toDomPrecision(height - (isSmallY ? 0 : targetSizeY))}
width={toDomPrecision(Math.max(1, width))}
height={toDomPrecision(Math.max(1, targetSizeY * 2))}
style={isDefaultCursor ? { cursor: getCursor('ns-resize', rotation) } : undefined}
{...bottomEvents}
/>
<rect
className={classNames('rs-transparent', {
'rs-hidden': hideEdgeTargets,
})}
data-wd="selection.resize.left"
aria-label="left target"
pointerEvents="all"
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 2 : targetSizeX))}
y={0}
height={toDomPrecision(Math.max(1, height))}
width={toDomPrecision(Math.max(1, targetSizeX * 2))}
style={isDefaultCursor ? { cursor: getCursor('ew-resize', rotation) } : undefined}
{...leftEvents}
/>
{/* Corner Targets */}
<rect
className={classNames('rs-transparent', {
'rs-hidden': hideTopLeftCorner,
})}
data-wd="selection.target.top-left"
aria-label="top-left target"
pointerEvents="all"
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 2 : targetSizeX * 1.5))}
y={toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY * 1.5))}
width={toDomPrecision(targetSizeX * 3)}
height={toDomPrecision(targetSizeY * 3)}
style={isDefaultCursor ? { cursor: getCursor('nwse-resize', rotation) } : undefined}
{...topLeftEvents}
/>
<rect
className={classNames('rs-transparent', {
'rs-hidden': hideTopRightCorner,
})}
data-wd="selection.target.top-right"
aria-label="top-right target"
pointerEvents="all"
x={toDomPrecision(width - (isSmallX ? 0 : targetSizeX * 1.5))}
y={toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY * 1.5))}
width={toDomPrecision(targetSizeX * 3)}
height={toDomPrecision(targetSizeY * 3)}
style={isDefaultCursor ? { cursor: getCursor('nesw-resize', rotation) } : undefined}
{...topRightEvents}
/>
<rect
className={classNames('rs-transparent', {
'rs-hidden': hideBottomRightCorner,
})}
data-wd="selection.target.bottom-right"
aria-label="bottom-right target"
pointerEvents="all"
x={toDomPrecision(width - (isSmallX ? targetSizeX : targetSizeX * 1.5))}
y={toDomPrecision(height - (isSmallY ? targetSizeY : targetSizeY * 1.5))}
width={toDomPrecision(targetSizeX * 3)}
height={toDomPrecision(targetSizeY * 3)}
style={isDefaultCursor ? { cursor: getCursor('nwse-resize', rotation) } : undefined}
{...bottomRightEvents}
/>
<rect
className={classNames('rs-transparent', {
'rs-hidden': hideBottomLeftCorner,
})}
data-wd="selection.target.bottom-left"
aria-label="bottom-left target"
pointerEvents="all"
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 3 : targetSizeX * 1.5))}
y={toDomPrecision(height - (isSmallY ? 0 : targetSizeY * 1.5))}
width={toDomPrecision(targetSizeX * 3)}
height={toDomPrecision(targetSizeY * 3)}
style={isDefaultCursor ? { cursor: getCursor('nesw-resize', rotation) } : undefined}
{...bottomLeftEvents}
/>
{/* Resize Handles */}
{showResizeHandles && (
<>
<rect
data-wd="selection.resize.top-left"
className={classNames('rs-corner-handle', {
'rs-hidden': hideTopLeftCorner,
})}
aria-label="top_left handle"
x={toDomPrecision(0 - size / 2)}
y={toDomPrecision(0 - size / 2)}
width={toDomPrecision(size)}
height={toDomPrecision(size)}
/>
<rect
data-wd="selection.resize.top-right"
className={classNames('rs-corner-handle', {
'rs-hidden': hideTopRightCorner,
})}
aria-label="top_right handle"
x={toDomPrecision(width - size / 2)}
y={toDomPrecision(0 - size / 2)}
width={toDomPrecision(size)}
height={toDomPrecision(size)}
/>
<rect
data-wd="selection.resize.bottom-right"
className={classNames('rs-corner-handle', {
'rs-hidden': hideBottomRightCorner,
})}
aria-label="bottom_right handle"
x={toDomPrecision(width - size / 2)}
y={toDomPrecision(height - size / 2)}
width={toDomPrecision(size)}
height={toDomPrecision(size)}
/>
<rect
data-wd="selection.resize.bottom-left"
className={classNames('rs-corner-handle', {
'rs-hidden': hideBottomLeftCorner,
})}
aria-label="bottom_left handle"
x={toDomPrecision(0 - size / 2)}
y={toDomPrecision(height - size / 2)}
width={toDomPrecision(size)}
height={toDomPrecision(size)}
/>
</>
)}
{showTextResizeHandles && (
<>
<rect
data-wd="selection.text-resize.left.handle"
className="rs-text-handle"
aria-label="bottom_left handle"
x={toDomPrecision(0 - size / 4)}
y={toDomPrecision(height / 2 - textHandleHeight / 2)}
rx={size / 4}
width={toDomPrecision(size / 2)}
height={toDomPrecision(textHandleHeight)}
/>
<rect
data-wd="selection.text-resize.right.handle"
className="rs-text-handle"
aria-label="bottom_left handle"
rx={size / 4}
x={toDomPrecision(width - size / 4)}
y={toDomPrecision(height / 2 - textHandleHeight / 2)}
width={toDomPrecision(size / 2)}
height={toDomPrecision(textHandleHeight)}
/>
</>
)}
{/* Crop Handles */}
{showCropHandles && (
<CropHandles
{...{
size,
width,
height,
hideAlternateHandles: hideAlternateCropHandles,
}}
<svg className="tl-svg-origin-container" ref={rSvg}>
<g data-wd="selection-foreground" className="tlui-selection__fg">
<rect
className={classNames('tlui-selection__fg__outline', { 'tl-hidden': !shouldDisplayBox })}
width={toDomPrecision(width)}
height={toDomPrecision(height)}
/>
)}
</g>
<RotateCornerHandle
data-wd="selection.rotate.top-left"
cx={0}
cy={0}
targetSize={targetSize}
corner="top_left_rotate"
cursor={isDefaultCursor ? getCursor('nwse-rotate', rotation) : undefined}
isHidden={hideRotateCornerHandles}
/>
<RotateCornerHandle
data-wd="selection.rotate.top-right"
cx={width + targetSize * 3}
cy={0}
targetSize={targetSize}
corner="top_right_rotate"
cursor={isDefaultCursor ? getCursor('nesw-rotate', rotation) : undefined}
isHidden={hideRotateCornerHandles}
/>
<RotateCornerHandle
data-wd="selection.rotate.bottom-left"
cx={0}
cy={height + targetSize * 3}
targetSize={targetSize}
corner="bottom_left_rotate"
cursor={isDefaultCursor ? getCursor('swne-rotate', rotation) : undefined}
isHidden={hideRotateCornerHandles}
/>
<RotateCornerHandle
data-wd="selection.rotate.bottom-right"
cx={width + targetSize * 3}
cy={height + targetSize * 3}
targetSize={targetSize}
corner="bottom_right_rotate"
cursor={isDefaultCursor ? getCursor('senw-rotate', rotation) : undefined}
isHidden={hideRotateCornerHandles}
/>{' '}
<MobileRotateHandle
data-wd="selection.rotate.mobile"
cx={isSmallX ? -targetSize * 1.5 : width / 2}
cy={isSmallX ? height / 2 : -targetSize * 1.5}
size={size}
isHidden={hideMobileRotateHandle}
/>
{/* Targets */}
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideEdgeTargets,
})}
data-wd="selection.resize.top"
aria-label="top target"
pointerEvents="all"
x={0}
y={toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY))}
width={toDomPrecision(Math.max(1, width))}
height={toDomPrecision(Math.max(1, targetSizeY * 2))}
style={isDefaultCursor ? { cursor: getCursor('ns-resize', rotation) } : undefined}
{...topEvents}
/>
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideEdgeTargets,
})}
data-wd="selection.resize.right"
aria-label="right target"
pointerEvents="all"
x={toDomPrecision(width - (isSmallX ? 0 : targetSizeX))}
y={0}
height={toDomPrecision(Math.max(1, height))}
width={toDomPrecision(Math.max(1, targetSizeX * 2))}
style={isDefaultCursor ? { cursor: getCursor('ew-resize', rotation) } : undefined}
{...rightEvents}
/>
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideEdgeTargets,
})}
data-wd="selection.resize.bottom"
aria-label="bottom target"
pointerEvents="all"
x={0}
y={toDomPrecision(height - (isSmallY ? 0 : targetSizeY))}
width={toDomPrecision(Math.max(1, width))}
height={toDomPrecision(Math.max(1, targetSizeY * 2))}
style={isDefaultCursor ? { cursor: getCursor('ns-resize', rotation) } : undefined}
{...bottomEvents}
/>
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideEdgeTargets,
})}
data-wd="selection.resize.left"
aria-label="left target"
pointerEvents="all"
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 2 : targetSizeX))}
y={0}
height={toDomPrecision(Math.max(1, height))}
width={toDomPrecision(Math.max(1, targetSizeX * 2))}
style={isDefaultCursor ? { cursor: getCursor('ew-resize', rotation) } : undefined}
{...leftEvents}
/>
{/* Corner Targets */}
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideTopLeftCorner,
})}
data-wd="selection.target.top-left"
aria-label="top-left target"
pointerEvents="all"
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 2 : targetSizeX * 1.5))}
y={toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY * 1.5))}
width={toDomPrecision(targetSizeX * 3)}
height={toDomPrecision(targetSizeY * 3)}
style={isDefaultCursor ? { cursor: getCursor('nwse-resize', rotation) } : undefined}
{...topLeftEvents}
/>
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideTopRightCorner,
})}
data-wd="selection.target.top-right"
aria-label="top-right target"
pointerEvents="all"
x={toDomPrecision(width - (isSmallX ? 0 : targetSizeX * 1.5))}
y={toDomPrecision(0 - (isSmallY ? targetSizeY * 2 : targetSizeY * 1.5))}
width={toDomPrecision(targetSizeX * 3)}
height={toDomPrecision(targetSizeY * 3)}
style={isDefaultCursor ? { cursor: getCursor('nesw-resize', rotation) } : undefined}
{...topRightEvents}
/>
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideBottomRightCorner,
})}
data-wd="selection.target.bottom-right"
aria-label="bottom-right target"
pointerEvents="all"
x={toDomPrecision(width - (isSmallX ? targetSizeX : targetSizeX * 1.5))}
y={toDomPrecision(height - (isSmallY ? targetSizeY : targetSizeY * 1.5))}
width={toDomPrecision(targetSizeX * 3)}
height={toDomPrecision(targetSizeY * 3)}
style={isDefaultCursor ? { cursor: getCursor('nwse-resize', rotation) } : undefined}
{...bottomRightEvents}
/>
<rect
className={classNames('tl-transparent', {
'tl-hidden': hideBottomLeftCorner,
})}
data-wd="selection.target.bottom-left"
aria-label="bottom-left target"
pointerEvents="all"
x={toDomPrecision(0 - (isSmallX ? targetSizeX * 3 : targetSizeX * 1.5))}
y={toDomPrecision(height - (isSmallY ? 0 : targetSizeY * 1.5))}
width={toDomPrecision(targetSizeX * 3)}
height={toDomPrecision(targetSizeY * 3)}
style={isDefaultCursor ? { cursor: getCursor('nesw-resize', rotation) } : undefined}
{...bottomLeftEvents}
/>
{/* Resize Handles */}
{showResizeHandles && (
<>
<rect
data-wd="selection.resize.top-left"
className={classNames('tl-corner-handle', {
'tl-hidden': hideTopLeftCorner,
})}
aria-label="top_left handle"
x={toDomPrecision(0 - size / 2)}
y={toDomPrecision(0 - size / 2)}
width={toDomPrecision(size)}
height={toDomPrecision(size)}
/>
<rect
data-wd="selection.resize.top-right"
className={classNames('tl-corner-handle', {
'tl-hidden': hideTopRightCorner,
})}
aria-label="top_right handle"
x={toDomPrecision(width - size / 2)}
y={toDomPrecision(0 - size / 2)}
width={toDomPrecision(size)}
height={toDomPrecision(size)}
/>
<rect
data-wd="selection.resize.bottom-right"
className={classNames('tl-corner-handle', {
'tl-hidden': hideBottomRightCorner,
})}
aria-label="bottom_right handle"
x={toDomPrecision(width - size / 2)}
y={toDomPrecision(height - size / 2)}
width={toDomPrecision(size)}
height={toDomPrecision(size)}
/>
<rect
data-wd="selection.resize.bottom-left"
className={classNames('tl-corner-handle', {
'tl-hidden': hideBottomLeftCorner,
})}
aria-label="bottom_left handle"
x={toDomPrecision(0 - size / 2)}
y={toDomPrecision(height - size / 2)}
width={toDomPrecision(size)}
height={toDomPrecision(size)}
/>
</>
)}
{showTextResizeHandles && (
<>
<rect
data-wd="selection.text-resize.left.handle"
className="tl-text-handle"
aria-label="bottom_left handle"
x={toDomPrecision(0 - size / 4)}
y={toDomPrecision(height / 2 - textHandleHeight / 2)}
rx={size / 4}
width={toDomPrecision(size / 2)}
height={toDomPrecision(textHandleHeight)}
/>
<rect
data-wd="selection.text-resize.right.handle"
className="tl-text-handle"
aria-label="bottom_left handle"
rx={size / 4}
x={toDomPrecision(width - size / 4)}
y={toDomPrecision(height / 2 - textHandleHeight / 2)}
width={toDomPrecision(size / 2)}
height={toDomPrecision(textHandleHeight)}
/>
</>
)}
{/* Crop Handles */}
{showCropHandles && (
<CropHandles
{...{
size,
width,
height,
hideAlternateHandles: hideAlternateCropHandles,
}}
/>
)}
</g>
</svg>
)
})
@ -427,7 +427,7 @@ export const RotateCornerHandle = function RotateCornerHandle({
const events = useSelectionEvents(corner)
return (
<rect
className={classNames('rs-transparent', 'rs-rotate-corner', { 'rs-hidden': isHidden })}
className={classNames('tl-transparent', 'tl-rotate-corner', { 'tl-hidden': isHidden })}
data-wd={dataWd}
aria-label={`${corner} target`}
pointerEvents="all"
@ -463,13 +463,13 @@ export const MobileRotateHandle = function RotateHandle({
<circle
data-wd={dataWd}
pointerEvents="all"
className={classNames('rs-transparent', 'rs-mobile-rotate__bg', { 'rs-hidden': isHidden })}
className={classNames('tl-transparent', 'tl-mobile-rotate__bg', { 'tl-hidden': isHidden })}
cx={cx}
cy={cy}
{...events}
/>
<circle
className={classNames('rs-mobile-rotate__fg', { 'rs-hidden': isHidden })}
className={classNames('tl-mobile-rotate__fg', { 'tl-hidden': isHidden })}
cx={cx}
cy={cy}
r={size / SQUARE_ROOT_PI}

View file

@ -111,7 +111,7 @@ export const Shape = track(function Shape({
<div
key={id}
ref={rContainer}
className="rs-shape"
className="tl-shape"
data-shape-type={shape.type}
draggable={false}
onPointerDown={events.onPointerDown}
@ -148,7 +148,7 @@ const CulledShape = React.memo(
const bounds = util.bounds(shape)
return (
<div
className="rs-shape__culled"
className="tl-shape__culled"
style={{
transform: `translate(${bounds.minX}px, ${bounds.minY}px)`,
width: bounds.width,

View file

@ -73,15 +73,17 @@ export const ShapeIndicator = React.memo(function ShapeIndicator({
)
return (
<g
className={classNames('rs-shape-indicator', {
'rs-shape-indicator__hinting': isHinting,
})}
transform={transform}
stroke={color ?? 'var(--color-selected)'}
>
<InnerIndicator app={app} id={id} />
</g>
<svg className="tl-svg-origin-container">
<g
className={classNames('tl-shape-indicator', {
'tl-shape-indicator__hinting': isHinting,
})}
transform={transform}
stroke={color ?? 'var(--color-selected)'}
>
<InnerIndicator app={app} id={id} />
</g>
</svg>
)
})

View file

@ -167,10 +167,10 @@ export const ICON_SIZES: Record<TLSizeType, number> = {
/** @public */
export const FONT_FAMILIES: Record<TLFontType, string> = {
draw: 'var(--rs-font-draw)',
sans: 'var(--rs-font-sans)',
serif: 'var(--rs-font-serif)',
mono: 'var(--rs-font-mono)',
draw: 'var(--tl-font-draw)',
sans: 'var(--tl-font-sans)',
serif: 'var(--tl-font-serif)',
mono: 'var(--tl-font-mono)',
}
/** @public */

View file

@ -63,7 +63,7 @@ export function useCursor() {
'useCursor',
() => {
const { type, rotation, color } = app.cursor
container.style.setProperty('--rs-cursor', getCursor(type, rotation, color))
container.style.setProperty('--tl-cursor', getCursor(type, rotation, color))
},
[app, container]
)

View file

@ -11,15 +11,15 @@ export function useDarkMode() {
React.useEffect(() => {
if (isDarkMode) {
container.setAttribute('data-color-mode', 'dark')
container.classList.remove('rs-theme__light')
container.classList.add('rs-theme__dark')
container.classList.remove('tl-theme__light')
container.classList.add('tl-theme__dark')
app.setCursor({
color: 'white',
})
} else {
container.setAttribute('data-color-mode', 'light')
container.classList.remove('rs-theme__dark')
container.classList.add('rs-theme__light')
container.classList.remove('tl-theme__dark')
container.classList.add('tl-theme__light')
app.setCursor({
color: 'black',
})

View file

@ -126,7 +126,7 @@ export const usePattern = () => {
}, [dpr])
const context = (
<defs>
<>
{backgroundUrls.map((item) => {
const key = item.zoom + (item.darkMode ? '_dark' : '_light')
return (
@ -141,7 +141,7 @@ export const usePattern = () => {
</pattern>
)
})}
</defs>
</>
)
return { context, isReady }

View file

@ -0,0 +1,25 @@
import { useLayoutEffect } from 'react'
export function useTransform(
ref: React.RefObject<HTMLElement | SVGElement>,
x?: number,
y?: number,
scale?: number,
rotate?: number
) {
useLayoutEffect(() => {
const elm = ref.current
if (!elm) return
if (x === undefined) return
if (scale !== undefined) {
if (rotate !== undefined) {
elm.style.transform = `translate(${x}px, ${y}px) scale(${scale}) rotate(${rotate}deg)`
} else {
elm.style.transform = `translate(${x}px, ${y}px) scale(${scale})`
}
} else {
elm.style.transform = `translate(${x}px, ${y}px)`
}
})
}

View file

@ -9,7 +9,7 @@ export function useZoomCss() {
const container = useContainer()
React.useEffect(() => {
const setScale = (s: number) => container.style.setProperty('--rs-zoom', s.toString())
const setScale = (s: number) => container.style.setProperty('--tl-zoom', s.toString())
const setScaleDebounced = debounce(setScale, 100)
const scheduler = new EffectScheduler('useZoomCss', () => {

View file

@ -96,7 +96,7 @@ export const EmbedDialog = track(function EmbedDialog({ onClose }: DialogProps)
}}
label="embed-dialog.back"
/>
<div className="tlui-spacer" />
<div className="tlui-embed__spacer" />
<Button label="embed-dialog.cancel" onClick={onClose} />
<Button
type="primary"

View file

@ -26,7 +26,7 @@ export function usePrint() {
prevStyleEl.current = style
// Random because this isn't for end users
const className = `rs-print-surface-${uniqueId()}`
const className = `tl-print-surface-${uniqueId()}`
el.className = className
// NOTE: Works in most envs except safari, needs further review

View file

@ -1,11 +1,7 @@
/* -------------------- UI Layers ------------------- */
.rs-container {
--sab: env(safe-area-inset-bottom);
.tl-container {
--layer-panels: 300;
--layer-menus: 400;
--layer-main-menu: 450;
--layer-overlays: 500;
--layer-dialogs: 600;
--layer-toasts: 650;
}
@ -25,6 +21,7 @@
user-select: none;
z-index: var(--layer-panels);
-webkit-transform: translate3d(0, 0, 0);
--sab: env(safe-area-inset-bottom);
}
.tlui-layout__top {
@ -1938,6 +1935,14 @@
/* Embed Dialog */
.tlui-embed__spacer {
flex-grow: 2;
min-height: 0px;
margin-left: calc(-1 * var(--space-4));
margin-top: calc(-1 * var(--space-4));
pointer-events: none;
}
.tlui-embed-dialog__list {
display: flex;
flex-direction: column;