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:
parent
986ffc1dd6
commit
dc16ae1b12
56 changed files with 898 additions and 804 deletions
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
71
apps/examples/src/11-user-presence/UserPresenceExample.tsx
Normal file
71
apps/examples/src/11-user-presence/UserPresenceExample.tsx
Normal 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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -114,6 +114,7 @@ export default function Example() {
|
|||
return (
|
||||
<div className="tldraw__editor">
|
||||
<Tldraw
|
||||
persistenceKey="custom-config"
|
||||
config={customTldrawConfig}
|
||||
autoFocus
|
||||
overrides={{
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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>
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}`}
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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`,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,5 +2,5 @@
|
|||
export type TLBackgroundComponent = () => JSX.Element | null
|
||||
|
||||
export function DefaultBackground() {
|
||||
return <div className="rs-background" />
|
||||
return <div className="tl-background" />
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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 &&
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
})
|
||||
|
||||
|
|
|
@ -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 */
|
||||
|
|
|
@ -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]
|
||||
)
|
||||
|
|
|
@ -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',
|
||||
})
|
||||
|
|
|
@ -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 }
|
||||
|
|
25
packages/editor/src/lib/hooks/useTransform.ts
Normal file
25
packages/editor/src/lib/hooks/useTransform.ts
Normal 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)`
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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', () => {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue