[improvement] Quality and UI cleanup (#846)

* remove sponsors, ui cleanup

* fix radius

* improve panel

* remove cursor spline animations

* migrate options

* Switch hrs to divider

* fix text color on menu dark mode

* Remove option for clone handles

* fix wheel

* remove unused translations
This commit is contained in:
Steve Ruiz 2022-07-23 15:05:48 +01:00 committed by GitHub
parent 014b07d417
commit f0f545806a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
78 changed files with 185 additions and 1191 deletions

View file

@ -6,16 +6,9 @@ declare const window: Window & { app: TldrawApp }
interface EditorProps {
id?: string
isUser?: boolean
isSponsor?: boolean
}
const Editor = ({
id = 'home',
isUser = false,
isSponsor = false,
...rest
}: EditorProps & Partial<TldrawProps>) => {
const Editor = ({ id = 'home', ...rest }: EditorProps & Partial<TldrawProps>) => {
const handleMount = React.useCallback((app: TldrawApp) => {
window.app = app
}, [])
@ -30,7 +23,6 @@ const Editor = ({
id={id}
autofocus
onMount={handleMount}
showSponsorLink={!isSponsor}
onAssetUpload={onAssetUpload}
{...fileSystemEvents}
{...rest}

File diff suppressed because one or more lines are too long

View file

@ -1,5 +1,4 @@
import { Tldraw, TldrawApp, TldrawProps, useFileSystem } from '@tldraw/tldraw'
import { useAccountHandlers } from 'hooks/useAccountHandlers'
import { useUploadAssets } from 'hooks/useUploadAssets'
import * as React from 'react'
import * as gtag from 'utils/gtag'
@ -8,16 +7,9 @@ declare const window: Window & { app: TldrawApp }
interface EditorProps {
id?: string
isUser?: boolean
isSponsor?: boolean
}
const Editor = ({
id = 'home',
isUser = false,
isSponsor = false,
...rest
}: EditorProps & Partial<TldrawProps>) => {
const Editor = ({ id = 'home', ...rest }: EditorProps & Partial<TldrawProps>) => {
const handleMount = React.useCallback((app: TldrawApp) => {
window.app = app
}, [])
@ -34,8 +26,6 @@ const Editor = ({
const fileSystemEvents = useFileSystem()
const { onSignIn, onSignOut } = useAccountHandlers()
const { onAssetUpload } = useUploadAssets()
return (
@ -45,9 +35,6 @@ const Editor = ({
autofocus
onMount={handleMount}
onPersist={handlePersist}
showSponsorLink={!isSponsor}
onSignIn={isSponsor ? undefined : onSignIn}
onSignOut={isUser ? onSignOut : undefined}
onAssetUpload={onAssetUpload}
{...fileSystemEvents}
{...rest}

View file

@ -1,39 +1,27 @@
import * as React from 'react'
import { RoomProvider } from '../utils/liveblocks'
import { Tldraw, useFileSystem } from '@tldraw/tldraw'
import { useAccountHandlers } from 'hooks/useAccountHandlers'
import { useMultiplayerAssets } from 'hooks/useMultiplayerAssets'
import { useMultiplayerState } from 'hooks/useMultiplayerState'
import { useUploadAssets } from 'hooks/useUploadAssets'
import React, { FC } from 'react'
import { styled } from 'styles'
interface Props {
roomId: string
isUser: boolean
isSponsor: boolean
}
const MultiplayerEditor: FC<Props> = ({
roomId,
isUser = false,
isSponsor = false,
}: {
roomId: string
isUser: boolean
isSponsor: boolean
}) => {
const MultiplayerEditor = ({ roomId }: Props) => {
return (
<RoomProvider id={roomId}>
<Editor roomId={roomId} isSponsor={isSponsor} isUser={isUser} />
<Editor roomId={roomId} />
</RoomProvider>
)
}
// Inner Editor
function Editor({ roomId, isUser, isSponsor }: Props) {
function Editor({ roomId }: Props) {
const fileSystemEvents = useFileSystem()
const { onSignIn, onSignOut } = useAccountHandlers()
const { error, ...events } = useMultiplayerState(roomId)
const { onAssetCreate, onAssetDelete } = useMultiplayerAssets()
const { onAssetUpload } = useUploadAssets()
@ -46,9 +34,6 @@ function Editor({ roomId, isUser, isSponsor }: Props) {
autofocus
disableAssets={false}
showPages={false}
showSponsorLink={!isSponsor}
onSignIn={isSponsor ? undefined : onSignIn}
onSignOut={isUser ? onSignOut : undefined}
onAssetCreate={onAssetCreate}
onAssetDelete={onAssetDelete}
onAssetUpload={onAssetUpload}

View file

@ -1,37 +1,25 @@
import * as React from 'react'
import { RoomProvider } from '../utils/liveblocks'
import { Tldraw, useFileSystem } from '@tldraw/tldraw'
import { useAccountHandlers } from 'hooks/useAccountHandlers'
import React, { FC } from 'react'
import { styled } from 'styles'
import { useReadOnlyMultiplayerState } from 'hooks/useReadOnlyMultiplayerState'
interface Props {
roomId: string
isUser: boolean
isSponsor: boolean
}
const ReadOnlyMultiplayerEditor: FC<Props> = ({
roomId,
isUser = false,
isSponsor = false,
}: {
roomId: string
isUser: boolean
isSponsor: boolean
}) => {
const ReadOnlyMultiplayerEditor = ({ roomId }: Props) => {
return (
<RoomProvider id={roomId}>
<ReadOnlyEditor roomId={roomId} isSponsor={isSponsor} isUser={isUser} />
<ReadOnlyEditor roomId={roomId} />
</RoomProvider>
)
}
// Inner Editor
function ReadOnlyEditor({ roomId, isUser, isSponsor }: Props) {
function ReadOnlyEditor({ roomId }: Props) {
const { onSaveProjectAs, onSaveProject } = useFileSystem()
const { onSignIn, onSignOut } = useAccountHandlers()
const { error, ...events } = useReadOnlyMultiplayerState(roomId)
if (error) return <LoadingScreen>Error: {error.message}</LoadingScreen>
@ -42,9 +30,6 @@ function ReadOnlyEditor({ roomId, isUser, isSponsor }: Props) {
autofocus
disableAssets={false}
showPages={false}
showSponsorLink={!isSponsor}
onSignIn={isSponsor ? undefined : onSignIn}
onSignOut={isUser ? onSignOut : undefined}
onSaveProjectAs={onSaveProjectAs}
onSaveProject={onSaveProject}
readOnly

View file

@ -1,14 +0,0 @@
import * as React from 'react'
import { signIn, signOut } from 'next-auth/react'
export function useAccountHandlers() {
const onSignIn = React.useCallback(() => {
signIn()
}, [])
const onSignOut = React.useCallback(() => {
signOut()
}, [])
return { onSignIn, onSignOut }
}

View file

@ -28,7 +28,6 @@
"@stitches/react": "^1.2.8",
"@tldraw/core": "*",
"@tldraw/tldraw": "*",
"@types/next-auth": "^3.15.0",
"@types/react": "^18.0.12",
"@types/react-dom": "^18.0.5",
"aws-sdk": "^2.1053.0",
@ -37,7 +36,6 @@
"lz-string": "^1.4.4",
"nanoid": "^3.3.4",
"next": "^12.1.6",
"next-auth": "^4.0.5",
"next-pwa": "^5.5.4",
"next-themes": "^0.0.15",
"react": "^18.1.0",
@ -49,4 +47,4 @@
"devDependencies": {
"next-transpile-modules": "^9.0.0"
}
}
}

View file

@ -1,39 +0,0 @@
import { isSignedInUserSponsoringMe } from 'utils/github'
import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'
import NextAuth from 'next-auth'
import GithubProvider from 'next-auth/providers/github'
export default function Auth(
req: NextApiRequest,
res: NextApiResponse
): ReturnType<NextApiHandler> {
return NextAuth(req, res, {
theme: {
colorScheme: 'light',
},
providers: [
GithubProvider({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
scope: 'read:user',
}),
],
secret: process.env.NEXTAUTH_SECRET,
callbacks: {
async redirect({ baseUrl }) {
return baseUrl
},
async signIn() {
return true
},
async session({ session, token }) {
if (token) {
session.isSponsor = await isSignedInUserSponsoringMe()
}
return session
},
},
})
}

View file

@ -1,99 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next'
const AV_SIZE = 32
const PADDING = 4
const COLS = 16
type SponsorResult = { avatarUrl: string; login: string }
type QueryResult = {
node: { sponsorEntity: { avatarUrl: string; login: string } }
}
function getXY(i: number) {
return [(i % COLS) * (AV_SIZE + PADDING), Math.floor(i / COLS) * (AV_SIZE + PADDING)]
}
export default async function GetSponsors(_req: NextApiRequest, res: NextApiResponse) {
const sponsorInfo = await fetch('https://api.github.com/graphql', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: 'bearer ' + process.env.GITHUB_SECRET,
},
body: JSON.stringify({
query: `{
viewer {
sponsors(first: 0) {
totalCount
}
sponsorshipsAsMaintainer(first: 100, orderBy: {
field:CREATED_AT,
direction:DESC
}) {
edges {
node {
sponsorEntity {
...on User {
avatarUrl
login
}
}
}
}
}
}
}`,
}),
}).then((res) => res.json())
// Get the total count of sponsors
const totalCount: number = sponsorInfo.data.viewer.sponsors.totalCount
// Map out the login and avatarUrl for each sponsor
const sponsors = (
sponsorInfo.data.viewer.sponsorshipsAsMaintainer.edges as QueryResult[]
).map<SponsorResult>((edge) => ({
login: edge.node.sponsorEntity.login,
avatarUrl: edge.node.sponsorEntity.avatarUrl?.replace(/&/g, '&amp;') ?? '',
}))
// If we're going to create a more link (see below), then make room for it if necessary
if (totalCount > 100 && sponsors.length % COLS <= 2) {
sponsors.pop()
sponsors.pop()
sponsors.pop()
}
// Generate images for each of the first 100 sponsors.
const avatars = sponsors
.map(({ avatarUrl, login }, i) => {
const [x, y] = getXY(i)
return `<image alt="${login}" href="${avatarUrl}" x="${x}" y="${y}" width="${AV_SIZE}" height="${AV_SIZE}"/>`
})
.join('')
// If there are more than 100 sponsors, generate some text to list how many more.
let more = ''
if (totalCount > sponsors.length) {
// More text
const [x, y] = getXY(sponsors.length)
const width = (AV_SIZE + PADDING) * 3
more = `<g transform="translate(${x},${y})"><text text-lenth="${width}" font-family="Arial" font-size="12px" font-weight="bold" text-anchor="middle" text-align="center" x="${
width / 2
}" y="${AV_SIZE / 2 + 3}">...and ${totalCount - sponsors.length} more!</text></g>`
}
const svgImage = `
<svg xmlns="http://www.w3.org/2000/svg"><a href="https://github.com/sponsors/steveruizok">${avatars}${more}</a></svg>`
res
.status(200)
.setHeader('Cache-Control', 'max-age=604800')
.setHeader('Content-Type', 'image/svg+xml')
.send(svgImage)
}

View file

@ -1,18 +1,11 @@
import type { GetServerSideProps } from 'next'
import { getSession } from 'next-auth/react'
import dynamic from 'next/dynamic'
import Head from 'next/head'
import { useRouter } from 'next/router'
import { FC, useMemo } from 'react'
import { useMemo } from 'react'
const Editor = dynamic(() => import('components/Editor'), { ssr: false }) as any
interface PageProps {
isUser: boolean
isSponsor: boolean
}
const Home: FC<PageProps> = ({ isUser, isSponsor }) => {
const Home = () => {
const { query } = useRouter()
const isExportMode = useMemo(() => 'exportMode' in query, [query])
@ -21,20 +14,9 @@ const Home: FC<PageProps> = ({ isUser, isSponsor }) => {
<Head>
<title>tldraw</title>
</Head>
<Editor id="home" isUser={isUser} isSponsor={isSponsor} showUI={!isExportMode} />
<Editor id="home" showUI={!isExportMode} />
</>
)
}
export default Home
export const getServerSideProps: GetServerSideProps = async (context) => {
const session = await getSession(context)
return {
props: {
isUser: session?.user ? true : false,
isSponsor: session?.isSponsor || false,
},
}
}

View file

@ -1,6 +1,5 @@
import * as React from 'react'
import type { GetServerSideProps } from 'next'
import { getSession } from 'next-auth/react'
import dynamic from 'next/dynamic'
const IFrameWarning = dynamic(() => import('components/IFrameWarning'), {
@ -13,27 +12,22 @@ const MultiplayerEditor = dynamic(() => import('components/MultiplayerEditor'),
interface RoomProps {
id: string
isSponsor: boolean
isUser: boolean
}
export default function Room({ id, isUser, isSponsor }: RoomProps) {
export default function Room({ id }: RoomProps) {
if (typeof window !== 'undefined' && window.self !== window.top) {
return <IFrameWarning url={`https://tldraw.com/r/${id}`} />
}
return <MultiplayerEditor isUser={isUser} isSponsor={isSponsor} roomId={id} />
return <MultiplayerEditor roomId={id} />
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const session = await getSession(context)
const id = context.query.id?.toString()
return {
props: {
id,
isUser: session?.user ? true : false,
isSponsor: session?.isSponsor ?? false,
},
}
}

View file

@ -1,205 +0,0 @@
import type { GetServerSideProps } from 'next'
import { getSession, signIn, signOut, useSession } from 'next-auth/react'
import Head from 'next/head'
import Link from 'next/link'
import React, { FC } from 'react'
import { styled } from 'styles'
const Sponsorware: FC = () => {
const { data, status } = useSession()
return (
<>
<Head>
<title>tldraw</title>
</Head>
<StyledOuterContent>
<StyledContent
size={{
'@sm': 'small',
}}
>
<h1>Tldraw (is sponsorware)</h1>
<p>
Hey, thanks for visiting <Link href="/">Tldraw</Link>, a tiny little drawing app by{' '}
<a
target="_blank"
rel="noreferrer nofollow noopener"
href="https://twitter.com/steveruizok"
>
steveruizok
</a>{' '}
and friends .
</p>
<video autoPlay muted playsInline onClick={(e) => e.currentTarget.play()}>
<source src="images/hello.mp4" type="video/mp4" />
</video>
<p>This project is currently: </p>
<ul>
<li>in development</li>
<li>only available for my sponsors</li>
</ul>
<p>
If you&apos;d like to try it out,{' '}
<a
href="https://github.com/sponsors/steveruizok"
target="_blank"
rel="noopener noreferrer"
>
sponsor me on GitHub
</a>{' '}
(at any level) and sign in below.
</p>
<StyledButtonGroup>
{data ? (
<>
<StyledButton variant="secondary" onClick={() => signOut()}>
Sign Out
</StyledButton>
<StyledDetail>
Signed in as {data.user?.name} ({data.user?.email}), but it looks like you&apos;re
not yet a sponsor.
<br />
Something wrong? Try <Link href="/">reloading the page</Link> or DM me on{' '}
<a
target="_blank"
rel="noreferrer nofollow noopener"
href="https://twitter.com/steveruizok"
>
Twitter
</a>
.
</StyledDetail>
</>
) : (
<>
<StyledButton variant="primary" onClick={() => signIn('github')}>
{status === 'loading' ? 'Loading...' : 'Sign in with GitHub'}
</StyledButton>
<StyledDetail>Already a sponsor? Just sign in to visit the app.</StyledDetail>
</>
)}
</StyledButtonGroup>
</StyledContent>
</StyledOuterContent>
</>
)
}
export default Sponsorware
export const getServerSideProps: GetServerSideProps = async (context) => {
const session = await getSession(context)
return {
props: {
session,
},
}
}
const StyledOuterContent = styled('div', {
backgroundColor: '$canvas',
padding: '8px 8px 64px 8px',
margin: '0 auto',
overflow: 'scroll',
position: 'fixed',
display: 'flex',
justifyContent: 'center',
alignItems: 'flex-start',
top: 0,
left: 0,
right: 0,
bottom: 0,
width: '100%',
height: '100%',
})
const StyledContent = styled('div', {
width: '720px',
padding: '8px 16px',
maxWidth: '100%',
backgroundColor: '$panel',
borderRadius: '4px',
boxShadow: '$12',
color: '$text',
fontSize: '$2',
fontFamily: '$body',
lineHeight: 1.5,
'& a': {
color: '$bounds',
backgroundColor: '$boundsBg',
padding: '2px 4px',
margin: '0 -3px',
borderRadius: '2px',
},
'& p': {
borderRadius: '8px',
},
'& video': {
maxWidth: '100%',
border: '1px solid $overlay',
borderRadius: '4px',
overflow: 'hidden',
margin: '16px 0',
},
'& iframe': {
border: 'none',
backgroundColor: 'none',
background: 'none',
},
variants: {
size: {
small: {
fontSize: '$3',
padding: '32px',
},
},
},
})
const StyledButtonGroup = styled('div', {
display: 'grid',
gap: '16px',
margin: '40px 0 32px 0',
})
const StyledDetail = styled('p', {
fontSize: '$2',
textAlign: 'center',
})
const StyledButton = styled('button', {
cursor: 'pointer',
width: '100%',
padding: '12px 0',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
font: '$ui',
fontSize: '$3',
color: '$panel',
border: 'none',
borderRadius: '4px',
variants: {
variant: {
primary: {
fontWeight: 'bold',
background: '$bounds',
color: '$panel',
boxShadow: '$4',
},
secondary: {
border: '1px solid $overlay',
background: 'transparent',
color: '$muted',
},
},
},
})

View file

@ -1,6 +1,5 @@
import * as React from 'react'
import type { GetServerSideProps } from 'next'
import { getSession } from 'next-auth/react'
import dynamic from 'next/dynamic'
import { Utils } from '@tldraw/core'
@ -14,27 +13,22 @@ const ReadOnlyMultiplayerEditor = dynamic(() => import('components/ReadOnlyMulti
interface RoomProps {
id: string
isSponsor: boolean
isUser: boolean
}
export default function Room({ id, isUser, isSponsor }: RoomProps) {
export default function Room({ id }: RoomProps) {
if (typeof window !== 'undefined' && window.self !== window.top) {
return <IFrameWarning url={`https://tldraw.com/v/${id}`} />
}
return <ReadOnlyMultiplayerEditor isUser={isUser} isSponsor={isSponsor} roomId={id} />
return <ReadOnlyMultiplayerEditor roomId={id} />
}
export const getServerSideProps: GetServerSideProps = async (context) => {
const session = await getSession(context)
const id = context.query.id?.toString()
return {
props: {
id: Utils.lns(id),
isUser: session?.user ? true : false,
isSponsor: session?.isSponsor ?? false,
},
}
}

View file

@ -1,4 +0,0 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M12.5 2.49538C12.2239 2.49538 12 2.71923 12 2.99538V5.49559H9.49979C9.22364 5.49559 8.99979 5.71945 8.99979 5.99559C8.99979 6.27173 9.22364 6.49559 9.49979 6.49559H12.5C12.7761 6.49559 13 6.27173 13 5.99559V2.99538C13 2.71923 12.7761 2.49538 12.5 2.49538Z" fill="black"/>
<path fillRule="evenodd" clipRule="evenodd" d="M7.69698 2.04877C6.62345 1.89773 5.52991 2.09968 4.58113 2.62417C3.63236 3.14867 2.87973 3.9673 2.43667 4.95673C1.99361 5.94616 1.8841 7.05278 2.12465 8.10985C2.3652 9.16693 2.94278 10.1172 3.77036 10.8175C4.59794 11.5177 5.63069 11.9301 6.713 11.9924C7.79531 12.0547 8.86855 11.7635 9.77101 11.1628C10.6735 10.5621 11.3563 9.68441 11.7165 8.66191C11.8083 8.40146 11.6715 8.11593 11.4111 8.02417C11.1506 7.93241 10.8651 8.06916 10.7733 8.32961C10.4851 9.14762 9.93888 9.84981 9.21691 10.3304C8.49493 10.811 7.63632 11.0439 6.77046 10.994C5.9046 10.9442 5.07839 10.6143 4.41631 10.0541C3.75424 9.49386 3.29217 8.73363 3.09972 7.88796C2.90728 7.04229 2.99488 6.15698 3.34934 5.36542C3.7038 4.57387 4.30591 3.91895 5.06494 3.49935C5.82398 3.07974 6.69882 2.91819 7.55765 3.03902C8.41649 3.15985 9.21279 3.55653 9.82658 4.16928L9.83745 4.17981L12.1576 6.35996C12.3588 6.54906 12.6753 6.53921 12.8644 6.33797C13.0535 6.13673 13.0436 5.8203 12.8424 5.63121L10.5276 3.4561C9.76111 2.69329 8.76794 2.19945 7.69698 2.04877Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,6 +0,0 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M2 4.65555C2 4.37941 2.22386 4.15555 2.5 4.15555H12.2C12.4761 4.15555 12.7 4.37941 12.7 4.65555C12.7 4.93169 12.4761 5.15555 12.2 5.15555H2.5C2.22386 5.15555 2 4.93169 2 4.65555Z" fill="black"/>
<path fillRule="evenodd" clipRule="evenodd" d="M6.27208 3C6.11885 3 5.97189 3.06087 5.86353 3.16923C5.75518 3.27758 5.6943 3.42454 5.6943 3.57778V4.15556H9.00542V3.57778C9.00542 3.42454 8.94454 3.27758 8.83619 3.16923C8.72783 3.06087 8.58087 3 8.42764 3H6.27208ZM10.0054 4.15556V3.57778C10.0054 3.15933 9.83919 2.75801 9.54329 2.46212C9.2474 2.16623 8.84609 2 8.42764 2H6.27208C5.85363 2 5.45232 2.16623 5.15642 2.46212C4.86053 2.75801 4.6943 3.15933 4.6943 3.57778V4.15556H3.57764C3.30149 4.15556 3.07764 4.37941 3.07764 4.65556V12.2C3.07764 12.6185 3.24387 13.0198 3.53976 13.3157C3.83565 13.6115 4.23696 13.7778 4.65541 13.7778H10.0443C10.4628 13.7778 10.8641 13.6115 11.16 13.3157C11.4559 13.0198 11.6221 12.6185 11.6221 12.2V4.65556C11.6221 4.37941 11.3982 4.15556 11.1221 4.15556H10.0054ZM4.07764 5.15556V12.2C4.07764 12.3532 4.13851 12.5002 4.24686 12.6086C4.35522 12.7169 4.50218 12.7778 4.65541 12.7778H10.0443C10.1975 12.7778 10.3445 12.7169 10.4529 12.6086C10.5612 12.5002 10.6221 12.3532 10.6221 12.2V5.15556H4.07764Z" fill="black"/>
<path fillRule="evenodd" clipRule="evenodd" d="M6.27246 6.85001C6.5486 6.85001 6.77246 7.07386 6.77246 7.35001V10.5833C6.77246 10.8595 6.5486 11.0833 6.27246 11.0833C5.99632 11.0833 5.77246 10.8595 5.77246 10.5833V7.35001C5.77246 7.07386 5.99632 6.85001 6.27246 6.85001Z" fill="black"/>
<path fillRule="evenodd" clipRule="evenodd" d="M8.42773 6.85001C8.70388 6.85001 8.92773 7.07386 8.92773 7.35001V10.5833C8.92773 10.8595 8.70388 11.0833 8.42773 11.0833C8.15159 11.0833 7.92773 10.8595 7.92773 10.5833V7.35001C7.92773 7.07386 8.15159 6.85001 8.42773 6.85001Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -1,4 +0,0 @@
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fillRule="evenodd" clipRule="evenodd" d="M2.5 2.49538C2.77614 2.49538 3 2.71923 3 2.99538V5.49559H5.50021C5.77636 5.49559 6.00021 5.71945 6.00021 5.99559C6.00021 6.27173 5.77636 6.49559 5.50021 6.49559H2.5C2.22386 6.49559 2 6.27173 2 5.99559V2.99538C2 2.71923 2.22386 2.49538 2.5 2.49538Z" fill="black"/>
<path fillRule="evenodd" clipRule="evenodd" d="M7.30302 2.04877C8.37655 1.89773 9.47009 2.09968 10.4189 2.62417C11.3676 3.14867 12.1203 3.9673 12.5633 4.95673C13.0064 5.94616 13.1159 7.05278 12.8753 8.10985C12.6348 9.16693 12.0572 10.1172 11.2296 10.8175C10.4021 11.5177 9.36931 11.9301 8.287 11.9924C7.20469 12.0547 6.13145 11.7635 5.22899 11.1628C4.32653 10.5621 3.64374 9.68441 3.2835 8.66191C3.19174 8.40146 3.32849 8.11593 3.58894 8.02417C3.84939 7.93241 4.13492 8.06916 4.22668 8.32961C4.51488 9.14762 5.06112 9.84981 5.78309 10.3304C6.50507 10.811 7.36368 11.0439 8.22954 10.994C9.0954 10.9442 9.92161 10.6143 10.5837 10.0541C11.2458 9.49386 11.7078 8.73363 11.9003 7.88796C12.0927 7.04229 12.0051 6.15698 11.6507 5.36542C11.2962 4.57387 10.6941 3.91895 9.93506 3.49935C9.17602 3.07974 8.30118 2.91819 7.44235 3.03902C6.58351 3.15985 5.78721 3.55653 5.17342 4.16928L5.16255 4.17981L2.84239 6.35996C2.64115 6.54906 2.32472 6.53921 2.13562 6.33797C1.94653 6.13673 1.95637 5.8203 2.15761 5.63121L4.47241 3.4561C5.23889 2.69329 6.23206 2.19945 7.30302 2.04877Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="35px" height="35px" viewBox="0 0 35 35" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fillRule="evenodd">
<g id="openhand">
<g id="bg-copy" fill="#FFFFFF" opacity="1">
<rect id="bg" x="0" y="0" width="35" height="35"></rect>
</g>
<path d="M13.5557,17.5742 C13.4577,17.1992 13.3597,16.7272 13.1497,16.0222 C12.9827,15.4652 12.8077,15.1632 12.6797,14.7892 C12.5247,14.3342 12.3767,14.0682 12.1837,13.6082 C12.0447,13.2792 11.8197,12.5602 11.7267,12.1682 C11.6077,11.6592 11.7597,11.2442 11.9707,10.9622 C12.2237,10.6232 12.9327,10.4722 13.3277,10.6112 C13.6987,10.7412 14.0717,11.1232 14.2437,11.3992 C14.5317,11.8592 14.6007,12.0312 14.9607,12.9412 C15.3537,13.9332 15.5247,14.8592 15.5717,15.1722 L15.6567,15.6242 C15.6557,15.5842 15.6137,14.5022 15.6127,14.4622 C15.5777,13.4332 15.5527,12.6392 15.5747,11.5232 C15.5767,11.3972 15.6387,10.9362 15.6587,10.8082 C15.7367,10.3082 15.9637,10.0082 16.3317,9.8292 C16.7437,9.6282 17.2577,9.6142 17.7327,9.8122 C18.1557,9.9852 18.3587,10.3622 18.4197,10.8342 C18.4337,10.9432 18.5137,11.8212 18.5127,11.9412 C18.4997,12.9662 18.5187,13.5822 18.5277,14.1152 C18.5317,14.3462 18.5307,15.7402 18.5447,15.5842 C18.6057,14.9282 18.6387,12.3952 18.8887,11.6422 C19.0327,11.2092 19.2937,10.8962 19.6827,10.7132 C20.1137,10.5102 20.7957,10.6432 21.0867,10.9562 C21.3717,11.2612 21.5327,11.6482 21.5687,12.1092 C21.6007,12.5142 21.5497,13.0062 21.5487,13.3542 C21.5487,14.2212 21.5277,14.6782 21.5117,15.4752 C21.5107,15.5132 21.4967,15.7732 21.5347,15.6572 C21.6287,15.3772 21.7227,15.1152 21.8007,14.9122 C21.8497,14.7872 22.0417,14.2982 22.1597,14.0532 C22.2737,13.8192 22.3707,13.6842 22.5747,13.3652 C22.7747,13.0522 22.9897,12.9172 23.2427,12.8042 C23.7827,12.5692 24.3517,12.9162 24.5437,13.3952 C24.6297,13.6102 24.5527,14.1082 24.5157,14.5002 C24.4547,15.1472 24.2617,15.8062 24.1637,16.1482 C24.0357,16.5952 23.8897,17.3832 23.8237,17.7492 C23.7517,18.1432 23.5897,19.1312 23.4647,19.5692 C23.3787,19.8702 23.0937,20.5472 22.8127,20.9532 C22.8127,20.9532 21.7387,22.2032 21.6207,22.7652 C21.5037,23.3282 21.5427,23.3322 21.5197,23.7302 C21.4957,24.1292 21.6407,24.6532 21.6407,24.6532 C21.6407,24.6532 20.8387,24.7572 20.4067,24.6872 C20.0157,24.6252 19.5317,23.8462 19.4067,23.6092 C19.2347,23.2812 18.8677,23.3442 18.7247,23.5862 C18.4997,23.9692 18.0157,24.6562 17.6737,24.6992 C17.0057,24.7832 15.6197,24.7292 14.5347,24.7192 C14.5347,24.7192 14.7197,23.7082 14.3077,23.3612 C14.0027,23.1012 13.4777,22.5772 13.1637,22.3012 L12.3317,21.3802 C12.0477,21.0202 11.7027,20.2872 11.0887,19.3952 C10.7407,18.8912 10.0617,18.3102 9.8047,17.8162 C9.5817,17.3912 9.4737,16.8622 9.6147,16.4912 C9.8397,15.8972 10.2897,15.5942 10.9767,15.6592 C11.4957,15.7092 11.8247,15.8652 12.2147,16.1962 C12.4397,16.3862 12.7877,16.7302 12.9647,16.9442 C13.1277,17.1392 13.1677,17.2202 13.3417,17.4532 C13.5717,17.7602 13.6437,17.9122 13.5557,17.5742" id="hand" fill="#FFFFFF"></path>
<path d="M13.5557,17.5742 C13.4577,17.1992 13.3597,16.7272 13.1497,16.0222 C12.9827,15.4652 12.8077,15.1632 12.6797,14.7892 C12.5247,14.3342 12.3767,14.0682 12.1837,13.6082 C12.0447,13.2792 11.8197,12.5602 11.7267,12.1682 C11.6077,11.6592 11.7597,11.2442 11.9707,10.9622 C12.2237,10.6232 12.9327,10.4722 13.3277,10.6112 C13.6987,10.7412 14.0717,11.1232 14.2437,11.3992 C14.5317,11.8592 14.6007,12.0312 14.9607,12.9412 C15.3537,13.9332 15.5247,14.8592 15.5717,15.1722 L15.6567,15.6242 C15.6557,15.5842 15.6137,14.5022 15.6127,14.4622 C15.5777,13.4332 15.5527,12.6392 15.5747,11.5232 C15.5767,11.3972 15.6387,10.9362 15.6587,10.8082 C15.7367,10.3082 15.9637,10.0082 16.3317,9.8292 C16.7437,9.6282 17.2577,9.6142 17.7327,9.8122 C18.1557,9.9852 18.3587,10.3622 18.4197,10.8342 C18.4337,10.9432 18.5137,11.8212 18.5127,11.9412 C18.4997,12.9662 18.5187,13.5822 18.5277,14.1152 C18.5317,14.3462 18.5307,15.7402 18.5447,15.5842 C18.6057,14.9282 18.6387,12.3952 18.8887,11.6422 C19.0327,11.2092 19.2937,10.8962 19.6827,10.7132 C20.1137,10.5102 20.7957,10.6432 21.0867,10.9562 C21.3717,11.2612 21.5327,11.6482 21.5687,12.1092 C21.6007,12.5142 21.5497,13.0062 21.5487,13.3542 C21.5487,14.2212 21.5277,14.6782 21.5117,15.4752 C21.5107,15.5132 21.4967,15.7732 21.5347,15.6572 C21.6287,15.3772 21.7227,15.1152 21.8007,14.9122 C21.8497,14.7872 22.0417,14.2982 22.1597,14.0532 C22.2737,13.8192 22.3707,13.6842 22.5747,13.3652 C22.7747,13.0522 22.9897,12.9172 23.2427,12.8042 C23.7827,12.5692 24.3517,12.9162 24.5437,13.3952 C24.6297,13.6102 24.5527,14.1082 24.5157,14.5002 C24.4547,15.1472 24.2617,15.8062 24.1637,16.1482 C24.0357,16.5952 23.8897,17.3832 23.8237,17.7492 C23.7517,18.1432 23.5897,19.1312 23.4647,19.5692 C23.3787,19.8702 23.0937,20.5472 22.8127,20.9532 C22.8127,20.9532 21.7387,22.2032 21.6207,22.7652 C21.5037,23.3282 21.5427,23.3322 21.5197,23.7302 C21.4957,24.1292 21.6407,24.6532 21.6407,24.6532 C21.6407,24.6532 20.8387,24.7572 20.4067,24.6872 C20.0157,24.6252 19.5317,23.8462 19.4067,23.6092 C19.2347,23.2812 18.8677,23.3442 18.7247,23.5862 C18.4997,23.9692 18.0157,24.6562 17.6737,24.6992 C17.0057,24.7832 15.6197,24.7292 14.5347,24.7192 C14.5347,24.7192 14.7197,23.7082 14.3077,23.3612 C14.0027,23.1012 13.4777,22.5772 13.1637,22.3012 L12.3317,21.3802 C12.0477,21.0202 11.7027,20.2872 11.0887,19.3952 C10.7407,18.8912 10.0617,18.3102 9.8047,17.8162 C9.5817,17.3912 9.4737,16.8622 9.6147,16.4912 C9.8397,15.8972 10.2897,15.5942 10.9767,15.6592 C11.4957,15.7092 11.8247,15.8652 12.2147,16.1962 C12.4397,16.3862 12.7877,16.7302 12.9647,16.9442 C13.1277,17.1392 13.1677,17.2202 13.3417,17.4532 C13.5717,17.7602 13.6437,17.9122 13.5557,17.5742" id="hand-border" stroke="#000000" stroke-width="0.75" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M20.5664,21.7344 L20.5664,18.2754" id="line3" stroke="#000000" stroke-width="0.75" stroke-linecap="round"></path>
<path d="M18.5508,21.7461 L18.5348,18.2731" id="line2" stroke="#000000" stroke-width="0.75" stroke-linecap="round"></path>
<path d="M16.5547,18.3047 L16.5757,21.7307" id="line1" stroke="#000000" stroke-width="0.75" stroke-linecap="round"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.2 KiB

View file

@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="35px" height="35px" viewBox="0 0 35 35" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fillRule="evenodd">
<g id="pointer">
<g id="bg" fill="#FFFFFF" opacity="0.00999999978">
<rect x="0" y="0" width="35" height="35"></rect>
</g>
<path d="M12,24.4219 L12,8.4069 L23.591,20.0259 L16.81,20.0259 L16.399,20.1499 L12,24.4219 Z" id="point-border" fill="#FFFFFF"></path>
<path d="M21.0845,25.0962 L17.4795,26.6312 L12.7975,15.5422 L16.4835,13.9892 L21.0845,25.0962 Z" id="stem-border" fill="#FFFFFF"></path>
<path d="M19.751,24.4155 L17.907,25.1895 L14.807,17.8155 L16.648,17.0405 L19.751,24.4155 Z" id="stem" fill="#000000"></path>
<path d="M13,10.814 L13,22.002 L15.969,19.136 L16.397,18.997 L21.165,18.997 L13,10.814 Z" id="point" fill="#000000"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1 KiB

View file

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="35px" height="35px" viewBox="0 0 35 35" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fillRule="evenodd">
<g id="resizenortheastsouthwest">
<g id="bg-copy" fill="#FFFFFF" opacity="1">
<rect id="bg" x="0" y="0" width="35" height="35"></rect>
</g>
<path d="M19.7432,17.0869 L15.6712,21.1549 L18.5002,23.9829 L10.0272,23.9699 L10.0142,15.4999 L12.8552,18.3419 L16.9302,14.2739 L18.3442,12.8589 L15.5002,10.0169 L23.9862,10.0169 L23.9862,18.5009 L21.1562,15.6739 L19.7432,17.0869 Z" id="resize-border" fill="#FFFFFF"></path>
<path d="M18.6826,16.7334 L14.2556,21.1574 L16.0836,22.9854 L11.0276,22.9694 L11.0136,17.9154 L12.8556,19.7564 L17.2836,15.3344 L19.7576,12.8594 L17.9136,11.0164 L22.9866,11.0164 L22.9866,16.0874 L21.1566,14.2594 L18.6826,16.7334 Z" id="resize" fill="#000000"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -35,15 +35,7 @@ export default function Develop() {
return (
<div className="tldraw">
<Tldraw
id="develop"
{...fileSystemEvents}
onMount={handleMount}
onSignIn={handleSignIn}
onSignOut={handleSignOut}
onPersist={handlePersist}
showSponsorLink={true}
/>
<Tldraw id="develop" {...fileSystemEvents} onMount={handleMount} onPersist={handlePersist} />
</div>
)
}

View file

@ -15,7 +15,7 @@ export default function Scroll() {
return (
<div style={{ height: 1600, width: 1600, padding: 200 }}>
<div style={{ width: '100%', height: '100%', position: 'relative' }}>
<Tldraw id="develop" onMount={handleMount} showSponsorLink={true} />
<Tldraw id="develop" onMount={handleMount} />
</div>
</div>
)

View file

@ -1,8 +1,6 @@
/* eslint-disable no-inner-declarations */
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import Vec from '@tldraw/vec'
import * as React from 'react'
import { useCursorAnimation } from '~hooks'
import type { TLShape, TLUser } from '~types'
interface UserProps {
@ -11,12 +9,17 @@ interface UserProps {
export function User({ user }: UserProps) {
const rCursor = React.useRef<SVGSVGElement>(null)
useCursorAnimation(rCursor, user.point, user.session)
React.useLayoutEffect(() => {
if (rCursor.current) {
rCursor.current.style.transform = `translate(${user.point[0]}px, ${user.point[1]}px)`
}
}, [user.point])
return (
<svg
ref={rCursor}
className="tl-absolute tl-user tl-counter-scaled"
className={`tl-absolute tl-user tl-counter-scaled ${user.session ? '' : 'tl-animated'}`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 35 35"
fill="none"

View file

@ -15,5 +15,4 @@ export * from './usePreventNavigationCss'
export * from './useBoundsEvents'
export * from './usePosition'
export * from './useKeyEvents'
export * from './useCursorAnimation'
export * from './usePerformanceCss'

View file

@ -1,163 +0,0 @@
import Vec from '@tldraw/vec'
import * as React from 'react'
type AnimationState = 'stopped' | 'idle' | 'animating'
type Animation = {
curve: boolean
from: number[]
to: number[]
start: number
distance: number
timeStamp: number
duration: number
}
export function useCursorAnimation(ref: any, point: number[], skip = false) {
const rState = React.useRef<AnimationState>('idle')
const rPrevPoint = React.useRef(point)
const rQueue = React.useRef<Animation[]>([])
const rTimestamp = React.useRef(performance.now())
const rLastRequestId = React.useRef<any>(0)
const rTimeoutId = React.useRef<any>(0)
const [spline] = React.useState(() => new Spline())
// When the point changes, add a new animation
React.useLayoutEffect(() => {
if (skip) {
const elm = ref.current
if (!elm) return
rState.current = 'stopped'
rPrevPoint.current = point
elm.style.setProperty('transform', `translate(${point[0]}px, ${point[1]}px)`)
return
}
const animateNext = (animation: Animation) => {
const start = performance.now()
function loop() {
const t = (performance.now() - start) / animation.duration
if (t <= 1) {
const elm = ref.current
if (!elm) return
const point = animation.curve
? spline.getSplinePoint(t + animation.start)
: Vec.lrp(animation.from, animation.to, t)
elm.style.setProperty('transform', `translate(${point[0]}px, ${point[1]}px)`)
rLastRequestId.current = requestAnimationFrame(loop)
return
}
const next = rQueue.current.shift()
if (next) {
rState.current = 'animating'
animateNext(next)
} else {
rState.current = 'idle'
rTimeoutId.current = setTimeout(() => {
rState.current = 'stopped'
}, 250)
}
}
loop()
}
const now = performance.now()
if (rState.current === 'stopped') {
rTimestamp.current = now
rPrevPoint.current = point
spline.clear()
}
spline.addPoint(point)
const animation: Animation = {
distance: spline.totalLength,
curve: spline.points.length > 3,
start: spline.points.length - 3,
from: rPrevPoint.current,
to: point,
timeStamp: now,
duration: Math.min(now - rTimestamp.current, 300),
}
rTimestamp.current = now
switch (rState.current) {
case 'stopped': {
rPrevPoint.current = point
rState.current = 'idle'
break
}
case 'idle': {
rState.current = 'animating'
animateNext(animation)
break
}
case 'animating': {
rPrevPoint.current = point
rQueue.current.push(animation)
break
}
}
return () => clearTimeout(rTimeoutId.current)
}, [skip, point, spline])
// React.useLayoutEffect(() => {
// const cursor = rCursor.current
// if (!cursor) return
// const [x, y] = user.point
// cursor.style.transform = `translate(${x}px, ${y}px)`
// }, [skip, point])
}
class Spline {
points: number[][] = []
lengths: number[] = []
totalLength = 0
private prev?: number[]
addPoint(point: number[]) {
if (this.prev) {
const length = Vec.dist(this.prev, point)
this.lengths.push(length)
this.totalLength += length
this.points.push(point)
}
this.prev = point
}
getSplinePoint(t: number): number[] {
const { points } = this
const l = points.length - 1
const d = Math.trunc(t)
const p1 = Math.min(d + 1, l)
const p2 = Math.min(p1 + 1, l)
const p3 = Math.min(p2 + 1, l)
const p0 = p1 - 1
t = t - d
const tt = t * t,
ttt = tt * t,
q1 = -ttt + 2 * tt - t,
q2 = 3 * ttt - 5 * tt + 2,
q3 = -3 * ttt + 4 * tt + t,
q4 = ttt - tt
if (!(points[p0] && points[p1] && points[p2] && points[p3])) {
return [0, 0]
}
return [
0.5 * (points[p0][0] * q1 + points[p1][0] * q2 + points[p2][0] * q3 + points[p3][0] * q4),
0.5 * (points[p0][1] * q1 + points[p1][1] * q2 + points[p2][1] * q3 + points[p3][1] * q4),
]
}
clear() {
this.points = []
this.totalLength = 0
}
}

View file

@ -249,6 +249,9 @@ export const TLCSS = css`
pointer-events: none;
will-change: transform;
}
.tl-animated {
transition: transform 200ms linear;
}
.tl-indicator {
fill: transparent;
stroke-width: calc(1.5px * var(--tl-scale));

View file

@ -36,12 +36,12 @@ export function useZoomEvents<T extends HTMLElement>(
e.preventDefault()
if (inputs.isPinching) return
const { offset } = normalizeWheel(e)
const [x, y, z] = normalizeWheel(e)
// alt+scroll or ctrl+scroll = zoom
if ((e.altKey || e.ctrlKey || e.metaKey) && e.buttons === 0) {
const point = inputs.pointer?.point ?? [bounds.width / 2, bounds.height / 2]
const delta = [...point, offset[1] * 0.618]
const delta = [...point, z * 0.618]
const info = inputs.pan(delta, e)
callbacks.onZoom?.({ ...info, delta }, e)
@ -52,9 +52,9 @@ export function useZoomEvents<T extends HTMLElement>(
const delta = Vec.mul(
e.shiftKey && !Utils.isDarwin
? // shift+scroll = pan horizontally
[offset[1], 0]
[y, 0]
: // scroll = pan vertically (or in any direction on a trackpad)
[...offset],
[x, y],
0.5
)
@ -146,45 +146,26 @@ export function useZoomEvents<T extends HTMLElement>(
}
// Reasonable defaults
const PIXEL_STEP = 10
const LINE_HEIGHT = 40
const PAGE_HEIGHT = 800
const MAX_ZOOM_STEP = 10
function normalizeWheel(event: any) {
let sX = 0,
sY = 0, // spinX, spinY
pX = 0,
pY = 0 // pixelX, pixelY
// Adapted from https://stackoverflow.com/a/13650579
function normalizeWheel(event: WheelEvent) {
const { deltaY, deltaX } = event
// Legacy
if ('detail' in event) sY = event.detail
if ('wheelDelta' in event) sY = -event.wheelDelta / 120
if ('wheelDeltaY' in event) sY = -event.wheelDeltaY / 120
if ('wheelDeltaX' in event) sX = -event.wheelDeltaX / 120
let deltaZ = 0
// side scrolling on FF with DOMMouseScroll
if ('axis' in event && event.axis === event.HORIZONTAL_AXIS) {
sX = sY
sY = 0
}
if (event.ctrlKey || event.metaKey) {
const signY = Math.sign(event.deltaY)
const absDeltaY = Math.abs(event.deltaY)
pX = 'deltaX' in event ? event.deltaX : sX * PIXEL_STEP
pY = 'deltaY' in event ? event.deltaY : sY * PIXEL_STEP
let dy = deltaY
if ((pX || pY) && event.deltaMode) {
if (event.deltaMode == 1) {
// delta in LINE units
pX *= LINE_HEIGHT
pY *= LINE_HEIGHT
} else {
// delta in PAGE units
pX *= PAGE_HEIGHT
pY *= PAGE_HEIGHT
if (absDeltaY > MAX_ZOOM_STEP) {
dy = MAX_ZOOM_STEP * signY
}
deltaZ = dy
}
// Fall-back if spin cannot be determined
if (pX && !sX) sX = pX < 1 ? -1 : 1
if (pY && !sY) sY = pY < 1 ? -1 : 1
return { spin: [sX, sY], offset: [pX, pY] }
return [deltaX, deltaY, deltaZ]
}

View file

@ -1426,11 +1426,6 @@ left past the initial left edge) then swap points on that axis.
}
}
static isMobileSize() {
if (typeof window === 'undefined') return false
return window.innerWidth < 768
}
static isMobileSafari() {
if (typeof window === 'undefined') return false
const ua = window.navigator.userAgent

View file

@ -74,11 +74,6 @@ export interface TldrawProps extends TDCallbacks {
*/
showTools?: boolean
/**
* (optional) Whether to show a sponsor link for Tldraw.
*/
showSponsorLink?: boolean
/**
* (optional) Whether to show the UI.
*/
@ -118,7 +113,6 @@ export function Tldraw({
readOnly = false,
disableAssets = false,
darkMode = false,
showSponsorLink,
onMount,
onChange,
onChangePresence,
@ -127,8 +121,6 @@ export function Tldraw({
onSaveProjectAs,
onOpenProject,
onOpenMedia,
onSignOut,
onSignIn,
onUndo,
onRedo,
onPersist,
@ -155,8 +147,6 @@ export function Tldraw({
onSaveProjectAs,
onOpenProject,
onOpenMedia,
onSignOut,
onSignIn,
onUndo,
onRedo,
onPersist,
@ -184,8 +174,6 @@ export function Tldraw({
onSaveProjectAs,
onOpenProject,
onOpenMedia,
onSignOut,
onSignIn,
onUndo,
onRedo,
onPersist,
@ -256,8 +244,6 @@ export function Tldraw({
onSaveProjectAs,
onOpenProject,
onOpenMedia,
onSignOut,
onSignIn,
onUndo,
onRedo,
onPersist,
@ -280,8 +266,6 @@ export function Tldraw({
onSaveProjectAs,
onOpenProject,
onOpenMedia,
onSignOut,
onSignIn,
onUndo,
onRedo,
onPersist,
@ -323,7 +307,6 @@ export function Tldraw({
showZoom={showZoom}
showTools={showTools}
showUI={showUI}
showSponsorLink={showSponsorLink}
readOnly={readOnly}
/>
</TldrawContext.Provider>
@ -341,7 +324,6 @@ interface InnerTldrawProps {
showStyles: boolean
showUI: boolean
showTools: boolean
showSponsorLink?: boolean
}
const InnerTldraw = React.memo(function InnerTldraw({
@ -353,7 +335,6 @@ const InnerTldraw = React.memo(function InnerTldraw({
showZoom,
showStyles,
showTools,
showSponsorLink,
readOnly,
showUI,
}: InnerTldrawProps) {
@ -438,8 +419,7 @@ const InnerTldraw = React.memo(function InnerTldraw({
const hideIndicators =
(isInSession && state.appState.status !== TDStatus.Brushing) || !isSelecting
const hideCloneHandles =
isInSession || !isSelecting || !settings.showCloneHandles || pageState.camera.zoom < 0.2
const hideCloneHandles = isInSession || !isSelecting || pageState.camera.zoom < 0.2
const translation = useTranslation(settings.language)
@ -551,7 +531,6 @@ const InnerTldraw = React.memo(function InnerTldraw({
showMultiplayerMenu={showMultiplayerMenu}
showStyles={showStyles}
showZoom={showZoom}
sponsor={showSponsorLink}
/>
<StyledSpacer />
{showTools && !readOnly && <ToolsPanel />}

View file

@ -2,11 +2,10 @@ import * as React from 'react'
import { styled } from '~styles'
export const Divider = styled('hr', {
height: 1,
marginTop: '$1',
marginRight: '-$0',
marginBottom: '$1',
marginLeft: '-$0',
height: 0,
paddingTop: 1,
width: 'calc(100%+8px)',
backgroundColor: '$hover',
border: 'none',
borderBottom: '1px solid $hover',
margin: '$2 -4px',
})

View file

@ -20,7 +20,7 @@ export const DMRadioItem = styled(RadioItem, {
isActive: {
true: {
backgroundColor: '$selected',
color: '$panel',
color: 'white',
},
false: {},
},

View file

@ -10,6 +10,7 @@ export const MenuContent = styled('div', {
minWidth: 180,
pointerEvents: 'all',
backgroundColor: '$panel',
border: '1px solid $panelContrast',
boxShadow: '$panel',
padding: '$2 $2',
borderRadius: '$3',

View file

@ -8,27 +8,34 @@ export const Panel = styled('div', {
padding: '$2',
border: '1px solid $panelContrast',
gap: 0,
overflow: 'hidden',
variants: {
side: {
center: {
borderRadius: '$4',
borderRadius: 9,
},
left: {
padding: 0,
borderTop: 0,
borderLeft: 0,
borderTopRightRadius: '$1',
borderBottomRightRadius: '$3',
borderBottomLeftRadius: '$1',
borderTopRightRadius: 0,
borderBottomRightRadius: 9,
borderBottomLeftRadius: 0,
},
right: {
padding: 0,
borderTop: 0,
borderRight: 0,
borderTopLeftRadius: '$1',
borderBottomLeftRadius: '$3',
borderBottomRightRadius: '$1',
borderTopLeftRadius: 0,
borderBottomLeftRadius: 9,
borderBottomRightRadius: 0,
},
},
},
'& hr': {
height: 10,
width: '100%',
backgroundColor: 'red',
border: 'none',
},
})

View file

@ -12,7 +12,6 @@ export interface RowButtonProps {
disabled?: boolean
kbd?: string
variant?: 'wide' | 'styleMenu'
isSponsor?: boolean
isActive?: boolean
isWarning?: boolean
hasIndicator?: boolean
@ -147,12 +146,6 @@ export const StyledRowButton = styled('button', {
width: 'auto',
},
},
isSponsor: {
true: {
color: '#eb30a2',
},
false: {},
},
isWarning: {
true: {
color: '$warn',
@ -165,34 +158,4 @@ export const StyledRowButton = styled('button', {
false: {},
},
},
compoundVariants: [
{
isActive: false,
isSponsor: true,
bp: 'small',
css: {
[`&:hover:not(:disabled) ${StyledRowButtonInner}`]: {
backgroundColor: '$sponsorContrast',
border: '1px solid $panel',
'& *[data-shy="true"]': {
opacity: 1,
},
},
},
},
{
isActive: false,
isSponsor: false,
bp: 'small',
css: {
[`&:hover:not(:disabled) ${StyledRowButtonInner}`]: {
backgroundColor: '$hover',
border: '1px solid $panel',
'& *[data-shy="true"]': {
opacity: 1,
},
},
},
},
],
})

View file

@ -10,7 +10,6 @@ export interface ToolButtonProps {
onDoubleClick?: () => void
disabled?: boolean
isActive?: boolean
isSponsor?: boolean
isToolLocked?: boolean
variant?: 'icon' | 'text' | 'circle' | 'primary'
children: React.ReactNode
@ -29,7 +28,6 @@ export const ToolButton = React.forwardRef<HTMLButtonElement, ToolButtonProps>(
isToolLocked = false,
disabled = false,
isActive = false,
isSponsor = false,
onKeyDown,
id,
...rest
@ -40,7 +38,6 @@ export const ToolButton = React.forwardRef<HTMLButtonElement, ToolButtonProps>(
<StyledToolButton
ref={ref}
isActive={isActive}
isSponsor={isSponsor}
variant={variant}
onClick={onClick}
disabled={disabled}
@ -129,9 +126,9 @@ export const StyledToolButton = styled('button', {
outline: 'none',
cursor: 'pointer',
pointerEvents: 'all',
border: 'none',
height: '40px',
width: '40px',
border: '1px solid $panel',
'-webkit-tap-highlight-color': 'transparent',
'tap-highlight-color': 'transparent',
@ -165,6 +162,7 @@ export const StyledToolButton = styled('button', {
padding: 0,
height: 32,
width: 32,
border: 'none',
[`& ${StyledToolButtonInner}`]: {
border: '1px solid $panelContrast',
borderRadius: '100%',
@ -176,13 +174,6 @@ export const StyledToolButton = styled('button', {
},
},
},
isSponsor: {
true: {
[`${StyledToolButtonInner}`]: {
backgroundColor: '$sponsorContrast',
},
},
},
isActive: {
true: {},
false: {},
@ -234,7 +225,6 @@ export const StyledToolButton = styled('button', {
css: {
[`&:hover:not(:disabled) ${StyledToolButtonInner}`]: {
backgroundColor: '$hover',
border: '1px solid $panel',
},
[`&:focus:not(:disabled) ${StyledToolButtonInner}`]: {
backgroundColor: '$hover',

View file

@ -1,14 +0,0 @@
import * as React from 'react'
export function HeartIcon() {
return (
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path
fill="none"
stroke="currentColor"
strokeWidth={2}
d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"
/>
</svg>
)
}

View file

@ -1,18 +0,0 @@
import * as React from 'react'
export function IsFilledIcon() {
return (
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<rect
x="4"
y="4"
width="16"
height="16"
rx="2"
strokeWidth="2"
fill="currentColor"
opacity=".9"
/>
</svg>
)
}

View file

@ -1,14 +0,0 @@
import * as React from 'react'
export function MultiplayerIcon() {
return (
<svg width="15" height="15" viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M5.73545 0.0494623C5.93029 -0.0395285 6.15915 -0.00663838 6.32104 0.133622L14.8111 7.48911C14.9804 7.63581 15.0432 7.87091 14.9696 8.08249C14.8959 8.29408 14.7007 8.4394 14.4769 8.44927L11.4564 8.58248L13.2249 12.4682C13.35 12.7431 13.2286 13.0675 12.9537 13.1927L10.9304 14.1145C10.6554 14.2397 10.331 14.1184 10.2057 13.8435L8.43441 9.95846L6.42883 12.0731L7.0482 13.434C7.17335 13.709 7.05195 14.0333 6.77702 14.1586L5.03829 14.9506C4.76335 15.0759 4.43894 14.9546 4.31361 14.6797L2.83886 11.445L1.13458 13.242C0.980476 13.4045 0.742914 13.4566 0.534904 13.3737C0.326893 13.2907 0.19043 13.0894 0.19043 12.8654V3.2218C0.19043 3.0076 0.315424 2.81309 0.510266 2.7241C0.705108 2.63511 0.933961 2.668 1.09586 2.80826L5.41561 6.55076V0.547159C5.41561 0.332956 5.54061 0.138453 5.73545 0.0494623ZM6.74741 10.147L6.50992 10.3974V10.1575V9.0621V8.94669V7.49882V1.74513L13.0561 7.41656L10.5986 7.52495C10.417 7.53296 10.2512 7.63066 10.1562 7.78567C10.1444 7.80505 10.1338 7.825 10.1245 7.84541C10.0596 7.98828 10.0589 8.15342 10.1247 8.2982V8.2982L12.0023 12.4236L10.9745 12.8919L9.09356 8.7663C9.0181 8.60079 8.86531 8.4836 8.68589 8.45361C8.50647 8.42363 8.32389 8.48478 8.19871 8.61676L8.019 8.80624L6.74741 10.147ZM5.41561 9.11037V7.99863L1.28473 4.41977V11.4934L2.60315 10.1033C2.72833 9.97131 2.91091 9.91016 3.09033 9.94014C3.26975 9.97013 3.42254 10.0873 3.498 10.2528L5.08241 13.728L5.82563 13.3895L4.24404 9.91441C4.16873 9.74894 4.18053 9.55687 4.27553 9.40186C4.37052 9.24685 4.53631 9.14915 4.71793 9.14114L5.41561 9.11037Z"
fill="currentColor"
/>
</svg>
)
}

View file

@ -1,19 +0,0 @@
import * as React from 'react'
export function QuestionMarkIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width="8"
height="16"
viewBox="0 0 4 8"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M1.15948 5.85192C1.05082 5.66856 0.972717 5.50556 0.925178 5.36295C0.884431 5.21354 0.864057 5.06074 0.864057 4.90454C0.864057 4.69401 0.921782 4.50046 1.03723 4.32389C1.15269 4.14731 1.2953 3.98093 1.46508 3.82473C1.63486 3.66174 1.80125 3.50214 1.96424 3.34594C2.13402 3.18295 2.27664 3.01317 2.39209 2.8366C2.50754 2.65324 2.56527 2.45629 2.56527 2.24576C2.56527 1.98769 2.48038 1.78056 2.31059 1.62436C2.1476 1.46137 1.93028 1.37988 1.65863 1.37988C1.48206 1.37988 1.32586 1.41044 1.19004 1.47156C1.05421 1.52589 0.911596 1.55305 0.762188 1.55305C0.572033 1.55305 0.429416 1.50551 0.334339 1.41044C0.239261 1.31536 0.191722 1.21009 0.191722 1.09464C0.191722 0.958818 0.252844 0.843366 0.375086 0.748289C0.50412 0.64642 0.701067 0.595485 0.965926 0.595485C1.30549 0.595485 1.6111 0.64642 1.88275 0.748289C2.1544 0.850157 2.38869 0.996169 2.58564 1.18632C2.78938 1.36969 2.94218 1.5904 3.04405 1.84847C3.15271 2.10654 3.20704 2.39517 3.20704 2.71436C3.20704 2.98601 3.14932 3.22031 3.03386 3.41725C2.91841 3.60741 2.7758 3.77719 2.60601 3.9266C2.44302 4.07601 2.27664 4.21862 2.10686 4.35445C1.93708 4.49027 1.79446 4.63628 1.67901 4.79248C1.56356 4.94189 1.50583 5.12186 1.50583 5.33239C1.50583 5.42067 1.50923 5.50896 1.51602 5.59725C1.5296 5.68553 1.54658 5.77042 1.56695 5.85192H1.15948ZM1.3734 7.8078C1.25116 7.8078 1.1391 7.78064 1.03723 7.72631C0.942156 7.67198 0.867452 7.59727 0.813122 7.50219C0.758792 7.40712 0.731627 7.30185 0.731627 7.1864C0.731627 7.06416 0.758792 6.95889 0.813122 6.87061C0.867452 6.77553 0.942156 6.70083 1.03723 6.6465C1.1391 6.59217 1.25116 6.565 1.3734 6.565C1.55677 6.565 1.70957 6.62612 1.83181 6.74837C1.95405 6.86382 2.01518 7.00983 2.01518 7.1864C2.01518 7.36297 1.95405 7.51238 1.83181 7.63462C1.70957 7.75008 1.55677 7.8078 1.3734 7.8078Z"
fill="currentColor"
/>
</svg>
)
}

View file

@ -1,16 +0,0 @@
import * as React from 'react'
export function RedoIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
width={32}
height={32}
viewBox="0 0 15 15"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path d="M4.32978 8.5081C4.32978 10.1923 5.70009 11.5625 7.38418 11.5625H8.46539C8.64456 11.5625 8.78975 11.4173 8.78975 11.2382V11.13C8.78975 10.9508 8.64457 10.8057 8.46539 10.8057H7.38418C6.11736 10.8057 5.08662 9.77492 5.08662 8.5081C5.08662 7.24128 6.11736 6.21054 7.38418 6.21054H9.37894L8.00515 7.58433C7.8576 7.73183 7.8576 7.97195 8.00515 8.11944C8.14833 8.26251 8.39751 8.2623 8.54036 8.11944L10.56 6.09971C10.6315 6.02824 10.6709 5.93321 10.6709 5.8321C10.6709 5.73106 10.6315 5.63598 10.56 5.56454L8.54025 3.54472C8.3974 3.40176 8.14801 3.40176 8.00513 3.54472C7.85758 3.69218 7.85758 3.93234 8.00513 4.07979L9.37892 5.45368H7.38418C5.70009 5.45368 4.32978 6.82393 4.32978 8.5081Z" />
</svg>
)
}

View file

@ -1,14 +1,18 @@
import * as React from 'react'
export function UndoIcon(props: React.SVGProps<SVGSVGElement>) {
export function UndoIcon({
flipHorizontal,
...props
}: React.SVGProps<SVGSVGElement> & { flipHorizontal?: boolean }) {
return (
<svg
width={32}
height={32}
width={24}
height={24}
viewBox="0 0 15 15"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
{...props}
transform={flipHorizontal ? 'scale(-1, 1)' : undefined}
>
<path d="M10.6707 8.5081C10.6707 10.1923 9.3004 11.5625 7.61631 11.5625H6.5351C6.35593 11.5625 6.21074 11.4173 6.21074 11.2382V11.13C6.21074 10.9508 6.35591 10.8057 6.5351 10.8057H7.61631C8.88313 10.8057 9.91387 9.77492 9.91387 8.5081C9.91387 7.24128 8.88313 6.21054 7.61631 6.21054H5.62155L6.99534 7.58433C7.14289 7.73183 7.14289 7.97195 6.99534 8.11944C6.85216 8.26251 6.60298 8.2623 6.46013 8.11944L4.44045 6.09971C4.36898 6.02824 4.32959 5.93321 4.32959 5.8321C4.32959 5.73106 4.36898 5.63598 4.44045 5.56454L6.46024 3.54472C6.60309 3.40176 6.85248 3.40176 6.99535 3.54472C7.14291 3.69218 7.14291 3.93234 6.99535 4.07979L5.62156 5.45368H7.61631C9.3004 5.45368 10.6707 6.82393 10.6707 8.5081Z" />
</svg>

View file

@ -4,15 +4,11 @@ export * from './DashDashedIcon'
export * from './DashDottedIcon'
export * from './DashDrawIcon'
export * from './DashSolidIcon'
export * from './IsFilledIcon'
export * from './RedoIcon'
export * from './TrashIcon'
export * from './UndoIcon'
export * from './SizeSmallIcon'
export * from './SizeMediumIcon'
export * from './SizeLargeIcon'
export * from './EraserIcon'
export * from './MultiplayerIcon'
export * from './DiscordIcon'
export * from './LineIcon'
export * from './QuestionMarkIcon'

View file

@ -6,14 +6,19 @@ import { styled } from '~styles'
import { useTldrawApp } from '~hooks'
import { TDSnapshot } from '~types'
import { breakpoints } from '~components/breakpoints'
import { GitHubLogoIcon, QuestionMarkIcon, TwitterLogoIcon } from '@radix-ui/react-icons'
import {
GitHubLogoIcon,
HeartFilledIcon,
QuestionMarkIcon,
TwitterLogoIcon,
} from '@radix-ui/react-icons'
import { RowButton } from '~components/Primitives/RowButton'
import { MenuContent } from '~components/Primitives/MenuContent'
import { DMContent, DMDivider } from '~components/Primitives/DropdownMenu'
import { SmallIcon } from '~components/Primitives/SmallIcon'
import { DiscordIcon } from '~components/Primitives/icons'
import { LanguageMenu } from '~components/TopPanel/LanguageMenu/LanguageMenu'
import { KeyboardShortcutDialog } from './keyboardShortcutDialog'
import { Divider } from '~components/Primitives/Divider'
const isDebugModeSelector = (s: TDSnapshot) => s.settings.isDebugMode
const dockPositionState = (s: TDSnapshot) => s.settings.dockPosition
@ -28,17 +33,17 @@ export function HelpPanel() {
return (
<Popover.Root>
<PopoverAnchor dir="ltr">
<Popover.Trigger asChild dir="ltr">
<Popover.Trigger dir="ltr" asChild>
<HelpButton side={side} debug={isDebugMode} bp={breakpoints}>
<QuestionMarkIcon />
</HelpButton>
</Popover.Trigger>
</PopoverAnchor>
<Popover.Content dir="ltr">
<Popover.Content dir="ltr" asChild>
<StyledContent style={{ visibility: isKeyboardShortcutsOpen ? 'hidden' : 'visible' }}>
<LanguageMenuDropdown />
<KeyboardShortcutDialog onOpenChange={setIsKeyboardShortcutsOpen} />
<DMDivider />
<Divider />
<Links />
</StyledContent>
</Popover.Content>
@ -60,9 +65,14 @@ const LanguageMenuDropdown = () => {
}
const linksData = [
{ id: 'github', title: 'Github', icon: GitHubLogoIcon, url: 'https://github.com/tldraw/tldraw' },
{ id: 'twitter', title: 'Twitter', icon: TwitterLogoIcon, url: 'https://twitter.com/tldraw' },
{ id: 'discord', title: 'Discord', icon: DiscordIcon, url: 'https://discord.gg/SBBEVCA4PG' },
{ id: 'github', icon: GitHubLogoIcon, url: 'https://github.com/tldraw/tldraw' },
{ id: 'twitter', icon: TwitterLogoIcon, url: 'https://twitter.com/tldraw' },
{ id: 'discord', icon: DiscordIcon, url: 'https://discord.gg/SBBEVCA4PG' },
{
id: 'become.a.sponsor',
icon: HeartFilledIcon,
url: 'https://github.com/sponsors/steveruizok',
},
]
const Links = () => {
@ -71,7 +81,7 @@ const Links = () => {
{linksData.map((item) => (
<a key={item.id} href={item.url} target="_blank" rel="nofollow">
<RowButton id={`TD-Link-${item.id}`} variant="wide">
{item.title}
<FormattedMessage id={item.id} />
<SmallIcon>
<item.icon />
</SmallIcon>
@ -92,10 +102,10 @@ const HelpButton = styled('button', {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
border: 'none',
backgroundColor: 'white',
backgroundColor: '$panel',
cursor: 'pointer',
boxShadow: '$panel',
border: '1px solid $panelContrast',
bottom: 10,
color: '$text',
variants: {

View file

@ -136,7 +136,7 @@ const StyledPanel = styled(Panel, {
bp: {
mobile: {
padding: '$0',
borderRadius: '$3',
borderRadius: '10px',
},
small: {
padding: '$2',

View file

@ -83,6 +83,7 @@ export function KeyboardShortcutDialog({
return (
<Dialog.Root onOpenChange={onOpenChange}>
{/* // todo: hide if no keyboard is attached */}
<Dialog.Trigger asChild>
<RowButton id="TD-HelpItem-Keyboard" variant="wide">
<FormattedMessage id="keyboard.shortcuts" />

View file

@ -6,6 +6,7 @@ import { SmallIcon } from '~components/Primitives/SmallIcon'
import { useTldrawApp } from '~hooks'
import { TDLanguage, TRANSLATIONS } from '~translations'
import { TDSnapshot } from '~types'
import { Divider } from '~components/Primitives/Divider'
const languageSelector = (s: TDSnapshot) => s.settings.language
@ -32,7 +33,7 @@ export function LanguageMenu() {
{label}
</DMCheckboxItem>
))}
<DMDivider />
<Divider />
<a
href="https://github.com/tldraw/tldraw/blob/main/guides/translation.md"
target="_blank"

View file

@ -1,5 +1,5 @@
import * as React from 'react'
import { ExitIcon, HamburgerMenuIcon } from '@radix-ui/react-icons'
import { HamburgerMenuIcon } from '@radix-ui/react-icons'
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
import { useTldrawApp } from '~hooks'
import { PreferencesMenu } from '../PreferencesMenu'
@ -10,7 +10,6 @@ import {
DMSubMenu,
DMTriggerIcon,
} from '~components/Primitives/DropdownMenu'
import { SmallIcon } from '~components/Primitives/SmallIcon'
import { useFileSystemHandlers } from '~hooks'
import { preventEvent } from '~components/preventEvent'
import { TDExportType, TDSnapshot } from '~types'
@ -18,7 +17,6 @@ import { Divider } from '~components/Primitives/Divider'
import { FormattedMessage, useIntl } from 'react-intl'
interface MenuProps {
sponsor: boolean | undefined
readOnly: boolean
}
@ -30,7 +28,7 @@ const disableAssetsSelector = (s: TDSnapshot) => {
return s.appState.disableAssets
}
export const Menu = React.memo(function Menu({ sponsor, readOnly }: MenuProps) {
export const Menu = React.memo(function Menu({ readOnly }: MenuProps) {
const app = useTldrawApp()
const intl = useIntl()
@ -80,14 +78,6 @@ export const Menu = React.memo(function Menu({ sponsor, readOnly }: MenuProps) {
app.exportJson()
}, [app])
const handleSignIn = React.useCallback(() => {
app.callbacks.onSignIn?.(app)
}, [app])
const handleSignOut = React.useCallback(() => {
app.callbacks.onSignOut?.(app)
}, [app])
const handleCut = React.useCallback(() => {
app.cut()
}, [app])
@ -123,8 +113,6 @@ export const Menu = React.memo(function Menu({ sponsor, readOnly }: MenuProps) {
app.callbacks.onSaveProjectAs ||
app.callbacks.onExport
const showSignInOutMenu = app.callbacks.onSignIn || app.callbacks.onSignOut
const hasSelection = numberOfSelectedIds > 0
return (
@ -186,7 +174,7 @@ export const Menu = React.memo(function Menu({ sponsor, readOnly }: MenuProps) {
>
<FormattedMessage id="redo" />
</DMItem>
<DMDivider dir="ltr" />
<Divider />
<DMItem
onSelect={preventEvent}
disabled={!hasSelection || readOnly}
@ -213,7 +201,7 @@ export const Menu = React.memo(function Menu({ sponsor, readOnly }: MenuProps) {
>
<FormattedMessage id="paste" />
</DMItem>
<DMDivider dir="ltr" />
<Divider />
<DMSubMenu
label={`${intl.formatMessage({ id: 'copy.as' })}...`}
size="small"
@ -251,7 +239,7 @@ export const Menu = React.memo(function Menu({ sponsor, readOnly }: MenuProps) {
</DMItem>
</DMSubMenu>
<DMDivider dir="ltr" />
<Divider />
<DMItem
onSelect={preventEvent}
onClick={handleSelectAll}
@ -268,7 +256,7 @@ export const Menu = React.memo(function Menu({ sponsor, readOnly }: MenuProps) {
>
<FormattedMessage id="select.none" />
</DMItem>
<DMDivider dir="ltr" />
<Divider />
<DMItem onSelect={handleDelete} disabled={!hasSelection} kbd="⌫" id="TD-MenuItem-Delete">
<FormattedMessage id="delete" />
</DMItem>
@ -315,26 +303,8 @@ export const Menu = React.memo(function Menu({ sponsor, readOnly }: MenuProps) {
<FormattedMessage id="zoom.to.selection" />
</DMItem>
</DMSubMenu>
<DMDivider dir="ltr" />
<Divider />
<PreferencesMenu />
{showSignInOutMenu && (
<>
<DMDivider dir="ltr" />{' '}
{app.callbacks.onSignIn && (
<DMItem onSelect={handleSignIn} id="TD-MenuItem-Sign_in">
<FormattedMessage id="menu.sign.in" />
</DMItem>
)}
{app.callbacks.onSignOut && (
<DMItem onSelect={handleSignOut} id="TD-MenuItem-Sign_out">
<FormattedMessage id="menu.sign.out" />
<SmallIcon>
<ExitIcon />
</SmallIcon>
</DMItem>
)}
</>
)}
</DMContent>
</DropdownMenu.Root>
)

View file

@ -9,6 +9,7 @@ import { TLDR } from '~state/TLDR'
import { Utils } from '@tldraw/core'
import { FormattedMessage } from 'react-intl'
import { MultiplayerIcon2 } from '~components/Primitives/icons/MultiplayerIcon2'
import { Divider } from '~components/Primitives/Divider'
const roomSelector = (state: TDSnapshot) => state.room
@ -124,7 +125,7 @@ export const MultiplayerMenu = React.memo(function MultiplayerMenu() {
<FormattedMessage id="copy.readonly.link" />
<SmallIcon>{copied ? <CheckIcon /> : <ClipboardIcon />}</SmallIcon>
</DMItem>
<DMDivider id="TD-Multiplayer-CopyInviteLinkDivider" />
<Divider />
<DMItem
id="TD-Multiplayer-CreateMultiplayerProject"
onClick={handleCreateMultiplayerProject}

View file

@ -10,6 +10,7 @@ import { SmallIcon } from '~components/Primitives/SmallIcon'
import { RowButton } from '~components/Primitives/RowButton'
import { ToolButton } from '~components/Primitives/ToolButton'
import { FormattedMessage, useIntl } from 'react-intl'
import { Divider } from '~components/Primitives/Divider'
const sortedSelector = (s: TDSnapshot) =>
Object.values(s.document.pages).sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0))
@ -148,7 +149,7 @@ function PageMenuContent({ onClose }: { onClose: () => void }) {
</ButtonWithOptions>
))}
</DropdownMenu.RadioGroup>
<DMDivider />
<Divider />
<DropdownMenu.Item onSelect={handleCreatePage} asChild>
<RowButton>
<span>

View file

@ -4,6 +4,7 @@ import { DMCheckboxItem, DMDivider, DMSubMenu } from '~components/Primitives/Dro
import { useTldrawApp } from '~hooks'
import { TDDockPosition, TDExportBackground, TDSnapshot } from '~types'
import { styled } from '~styles'
import { Divider } from '~components/Primitives/Divider'
const settingsSelector = (s: TDSnapshot) => s.settings
@ -94,7 +95,7 @@ export function PreferencesMenu() {
>
<FormattedMessage id="preferences.debug.mode" />
</DMCheckboxItem>
<DMDivider />
<Divider />
<DMCheckboxItem
checked={settings.showGrid}
onCheckedChange={toggleGrid}
@ -117,34 +118,6 @@ export function PreferencesMenu() {
>
<FormattedMessage id="preferences.keep.stylemenu.open" />
</DMCheckboxItem>
<DMCheckboxItem
checked={settings.isSnapping}
onCheckedChange={toggleisSnapping}
id="TD-MenuItem-Preferences-Always_Show_Snaps"
>
<FormattedMessage id="preferences.always.show.snaps" />
</DMCheckboxItem>
<DMCheckboxItem
checked={settings.showRotateHandles}
onCheckedChange={toggleRotateHandle}
id="TD-MenuItem-Preferences-Rotate_Handles"
>
<FormattedMessage id="preferences.rotate.handles" />
</DMCheckboxItem>
<DMCheckboxItem
checked={settings.showBindingHandles}
onCheckedChange={toggleBoundShapesHandle}
id="TD-MenuItem-Preferences-Binding_Handles"
>
<FormattedMessage id="preferences.binding.handles" />
</DMCheckboxItem>
<DMCheckboxItem
checked={settings.showCloneHandles}
onCheckedChange={toggleCloneControls}
id="TD-MenuItem-Preferences-Clone_Handles"
>
<FormattedMessage id="preferences.clone.handles" />
</DMCheckboxItem>
<DMSubMenu label={intl.formatMessage({ id: 'dock.position' })} overflow={false}>
{DockPosition.map((position) => (
<DMCheckboxItem

View file

@ -241,7 +241,7 @@ export const StyleMenu = React.memo(function ColorMenu() {
size={18}
strokeWidth={2.5}
fill={
displayedStyle.isFilled ? fills.light[style as ColorStyle] : 'transparent'
displayedStyle.isFilled ? fills[theme][style as ColorStyle] : 'transparent'
}
stroke={strokes.light[style as ColorStyle]}
/>
@ -337,7 +337,7 @@ export const StyleMenu = React.memo(function ColorMenu() {
)}
</>
)}
<DMDivider />
<Divider />
<DMCheckboxItem
variant="styleMenu"
checked={keepOpen}
@ -403,6 +403,7 @@ const OverlapIcons = styled('div', {
gridColumn: 1,
gridRow: 1,
},
backgroundColor: '$panel',
})
const FontIcon = styled('div', {

View file

@ -6,7 +6,7 @@ import { ZoomMenu } from './ZoomMenu'
import { StyleMenu } from './StyleMenu'
import { Panel } from '~components/Primitives/Panel'
import { ToolButton } from '~components/Primitives/ToolButton'
import { RedoIcon, UndoIcon } from '~components/Primitives/icons'
import { UndoIcon } from '~components/Primitives/icons'
import { useTldrawApp } from '~hooks'
import { MultiplayerMenu } from './MultiplayerMenu'
@ -17,7 +17,6 @@ interface TopPanelProps {
showStyles: boolean
showZoom: boolean
showMultiplayerMenu: boolean
sponsor?: boolean
}
export function TopPanel({
@ -26,7 +25,6 @@ export function TopPanel({
showMenu,
showStyles,
showZoom,
sponsor,
showMultiplayerMenu,
}: TopPanelProps) {
const app = useTldrawApp()
@ -35,7 +33,7 @@ export function TopPanel({
<StyledTopPanel>
{(showMenu || showPages) && (
<Panel side="left" id="TD-MenuPanel">
{showMenu && <Menu sponsor={sponsor} readOnly={readOnly} />}
{showMenu && <Menu readOnly={readOnly} />}
{showMultiplayerMenu && <MultiplayerMenu />}
{showPages && <PageMenu />}
</Panel>
@ -47,12 +45,11 @@ export function TopPanel({
<ReadOnlyLabel>Read Only</ReadOnlyLabel>
) : (
<>
{' '}
<ToolButton>
<UndoIcon onClick={app.undo} />
</ToolButton>
<ToolButton>
<RedoIcon onClick={app.redo} />
<UndoIcon onClick={app.redo} flipHorizontal />
</ToolButton>
</>
)}

View file

@ -30,10 +30,10 @@ export const ZoomMenu = React.memo(function ZoomMenu() {
<FormattedMessage id="zoom.out" />
</DMItem>
<DMItem onSelect={preventEvent} onClick={app.resetZoom} kbd="⇧0" id="TD-Zoom-Zoom_To_100%">
<FormattedMessage id="to" /> 100%
<FormattedMessage id="zoom.to" /> 100%
</DMItem>
<DMItem onSelect={preventEvent} onClick={app.zoomToFit} kbd="⇧1" id="TD-Zoom-To_Fit">
<FormattedMessage id="to.fit" />
<FormattedMessage id="zoom.to.fit" />
</DMItem>
<DMItem
onSelect={preventEvent}
@ -41,7 +41,7 @@ export const ZoomMenu = React.memo(function ZoomMenu() {
kbd="⇧2"
id="TD-Zoom-To_Selection"
>
<FormattedMessage id="to.selection" />
<FormattedMessage id="zoom.to.selection" />
</DMItem>
</DMContent>
</DropdownMenu.Root>

View file

@ -119,14 +119,6 @@ export interface TDCallbacks {
* (optional) A callback to run when the opens a file to upload.
*/
onOpenMedia?: (app: TldrawApp) => void
/**
* (optional) A callback to run when the user signs in via the menu.
*/
onSignIn?: (app: TldrawApp) => void
/**
* (optional) A callback to run when the user signs out via the menu.
*/
onSignOut?: (app: TldrawApp) => void
/**
* (optional) A callback to run when the state is patched.
*/
@ -608,73 +600,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
/* ----------- Managing Multiplayer State ----------- */
private justSent = false
private prevShapes = this.page.shapes
private prevBindings = this.page.bindings
private prevAssets = this.document.assets
// private broadcastPageChanges = () => {
// const visited = new Set<string>()
// const changedShapes: Record<string, TDShape | undefined> = {}
// const changedBindings: Record<string, TDBinding | undefined> = {}
// const changedAssets: Record<string, TDAsset | undefined> = {}
// this.shapes.forEach((shape) => {
// visited.add(shape.id)
// if (this.prevShapes[shape.id] !== shape) {
// changedShapes[shape.id] = shape
// }
// })
// Object.keys(this.prevShapes)
// .filter((id) => !visited.has(id))
// .forEach((id) => {
// // After visiting all the current shapes, if we haven't visited a
// // previously present shape, then it was deleted
// changedShapes[id] = undefined
// })
// this.bindings.forEach((binding) => {
// visited.add(binding.id)
// if (this.prevBindings[binding.id] !== binding) {
// changedBindings[binding.id] = binding
// }
// })
// Object.keys(this.prevBindings)
// .filter((id) => !visited.has(id))
// .forEach((id) => {
// // After visiting all the current bindings, if we haven't visited a
// // previously present shape, then it was deleted
// changedBindings[id] = undefined
// })
// this.assets.forEach((asset) => {
// visited.add(asset.id)
// if (this.prevAssets[asset.id] !== asset) {
// changedAssets[asset.id] = asset
// }
// })
// Object.keys(this.prevAssets)
// .filter((id) => !visited.has(id))
// .forEach((id) => {
// changedAssets[id] = undefined
// })
// // Only trigger update if shapes or bindings have changed
// if (
// Object.keys(changedBindings).length > 0 ||
// Object.keys(changedShapes).length > 0 ||
// Object.keys(changedAssets).length > 0
// ) {
// this.justSent = true
// this.callbacks.onChangePage?.(this, changedShapes, changedBindings, changedAssets,)
// this.prevShapes = this.page.shapes
// this.prevBindings = this.page.bindings
// this.prevAssets = this.document.assets
// }
// }
getReservedContent = (coreReservedIds: string[], pageId = this.currentPageId) => {
const { bindings } = this.document.pages[pageId]
@ -807,9 +732,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
// will have changed. This is important because we want to restore
// related shapes that may not have changed on our side, but which
// were deleted on the server.
this.prevShapes = shapes
this.prevBindings = bindings
this.prevAssets = assets
const nextShapes = {
...shapes,
@ -914,8 +836,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
})
this.state.document = next.document
// this.prevShapes = nextShapes
// this.prevBindings = nextBindings
return next
}, true)
@ -1941,7 +1861,6 @@ export class TldrawApp extends StateManager<TDSnapshot> {
break
}
case 'text/plain': {
console.log(str)
if (str.startsWith('<svg')) {
getSvgFromText(str)
} else {
@ -2153,30 +2072,30 @@ export class TldrawApp extends StateManager<TDSnapshot> {
svg.setAttribute('height', commonBounds.height.toString())
// Set export background
const exportBackground: TDExportBackground = this.settings.exportBackground;
const darkBackground = '#212529';
const exportBackground: TDExportBackground = this.settings.exportBackground
const darkBackground = '#212529'
const lightBackground = 'rgb(248, 249, 250)'
switch(exportBackground) {
switch (exportBackground) {
case TDExportBackground.Auto: {
svg.style.setProperty(
'background-color',
this.settings.isDarkMode ? darkBackground : lightBackground
)
break;
break
}
case TDExportBackground.Dark: {
svg.style.setProperty('background-color', darkBackground)
break;
break
}
case TDExportBackground.Light: {
svg.style.setProperty('background-color', lightBackground)
break;
break
}
case TDExportBackground.Transparent:
case TDExportBackground.Transparent:
default: {
svg.style.setProperty('background-color', 'transparent')
break;
break
}
}
@ -2384,7 +2303,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
opts
const svg = await this.getSvg(ids, {
includeFonts: format !== TDExportType.SVG
includeFonts: format !== TDExportType.SVG,
})
if (!svg) return
@ -4166,7 +4085,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
getShapeUtil = TLDR.getShapeUtil
static version = 15.4
static version = 15.5
static defaultDocument: TDDocument = {
id: 'doc',
@ -4213,7 +4132,7 @@ export class TldrawApp extends StateManager<TDSnapshot> {
showGrid: false,
language: 'en',
dockPosition: 'bottom',
exportBackground: TDExportBackground.Transparent
exportBackground: TDExportBackground.Transparent,
},
appState: {
status: TDStatus.Idle,

View file

@ -50,7 +50,7 @@ TldrawTestApp {
"shapes": Object {},
},
},
"version": 15.4,
"version": 15.5,
},
"settings": Object {
"dockPosition": "bottom",
@ -202,7 +202,7 @@ TldrawTestApp {
},
},
},
"version": 15.4,
"version": 15.5,
},
"settings": Object {
"dockPosition": "bottom",
@ -375,7 +375,7 @@ TldrawTestApp {
"shapes": Object {},
},
},
"version": 15.4,
"version": 15.5,
},
"settings": Object {
"dockPosition": "bottom",
@ -495,10 +495,7 @@ TldrawTestApp {
"pointShape": [Function],
"pointer": -1,
"pressKey": [Function],
"prevAssets": Object {},
"prevBindings": Object {},
"prevSelectedIds": Array [],
"prevShapes": Object {},
"previousPoint": Array [
0,
0,

View file

@ -1,5 +1,13 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { Decoration, FontStyle, TDDocument, TDShapeType, TDSnapshot, TextShape } from '~types'
import {
Decoration,
FontStyle,
TDDocument,
TDExportBackground,
TDShapeType,
TDSnapshot,
TextShape,
} from '~types'
export function migrate(state: TDSnapshot, newVersion: number): TDSnapshot {
const { document, settings } = state
@ -122,6 +130,10 @@ export function migrate(state: TDSnapshot, newVersion: number): TDSnapshot {
settings.dockPosition = 'bottom'
}
if (version < 15.5) {
settings.exportBackground = TDExportBackground.Transparent
}
// Cleanup
Object.values(document.pageStates).forEach((pageState) => {
pageState.selectedIds = pageState.selectedIds.filter((id) => {

View file

@ -23,7 +23,7 @@ export class ImageUtil extends TDShapeUtil<T, E> {
isAspectRatioLocked = true
showCloneHandles = true
showCloneHandles = false
getShape = (props: Partial<T>): T => {
return Utils.deepMerge<T>(
@ -46,6 +46,7 @@ export class ImageUtil extends TDShapeUtil<T, E> {
Component = TDShapeUtil.Component<T, E, TDMeta>(
({ shape, asset = { src: '' }, isBinding, isGhost, meta, events, onShapeChange }, ref) => {
const { size, style } = shape
const { bindingDistance } = this
const rImage = React.useRef<HTMLImageElement>(null)
const rWrapper = React.useRef<HTMLDivElement>(null)
@ -65,10 +66,10 @@ export class ImageUtil extends TDShapeUtil<T, E> {
className="tl-binding-indicator"
style={{
position: 'absolute',
top: `calc(${-this.bindingDistance}px * var(--tl-zoom))`,
left: `calc(${-this.bindingDistance}px * var(--tl-zoom))`,
width: `calc(100% + ${this.bindingDistance * 2}px * var(--tl-zoom))`,
height: `calc(100% + ${this.bindingDistance * 2}px * var(--tl-zoom))`,
top: `calc(${-bindingDistance}px * var(--tl-zoom))`,
left: `calc(${-bindingDistance}px * var(--tl-zoom))`,
width: `calc(100% + ${bindingDistance * 2}px * var(--tl-zoom))`,
height: `calc(100% + ${bindingDistance * 2}px * var(--tl-zoom))`,
backgroundColor: 'var(--tl-selectFill)',
}}
/>
@ -85,7 +86,6 @@ export class ImageUtil extends TDShapeUtil<T, E> {
src={(asset as TDImageAsset).src}
alt="tl_image_asset"
draggable={false}
// onLoad={onImageLoad}
/>
</Wrapper>
</HTMLContainer>

View file

@ -20,7 +20,7 @@ export class VideoUtil extends TDShapeUtil<T, E> {
canEdit = true
canClone = true
isAspectRatioLocked = true
showCloneHandles = true
showCloneHandles = false
isStateful = true // don't unmount
getShape = (props: Partial<T>): T => {

View file

@ -15,8 +15,6 @@ const { styled, createTheme } = createStitches({
panelContrast: '#ffffff',
selected: 'rgba(66, 133, 244, 1.000)',
selectedContrast: '#fefefe',
sponsor: '#ec6cb9',
sponsorContrast: '#ec6cb944',
text: '#333333',
tooltip: '#1d1d1d',
tooltipContrast: '#ffffff',

View file

@ -10,8 +10,6 @@
"zoom.in": "تكبير",
"zoom.out": "تصغير",
"to": "إلى",
"to.selection": "للاختيار",
"to.fit": "تناسب",
"menu.tools": "أدوات",
"menu.transform": "التحويلات",
"menu.file": "ملف",
@ -20,7 +18,6 @@
"menu.preferences": "التفضيلات",
"menu.sign.in": "تسجيل الدخول",
"menu.sign.out": "خروج",
"sponsored": "برعاية",
"become.a.sponsor": "كن راعياً",
"zoom.to.content": "العودة إلى المحتوى",
"zoom.to.selection": "تكبير للتحديد",
@ -101,4 +98,4 @@
"keyboard.shortcuts": "اختصارات لوحة المفاتيح",
"search": "بحث",
"loading": "{dots}تحميل "
}
}

View file

@ -10,15 +10,12 @@
"zoom.in": "Zoom ind",
"zoom.out": "Zoom ud",
"to": "til",
"to.selection": "Til valgte",
"to.fit": "Til lærred",
"menu.file": "Fil",
"menu.edit": "Rediger",
"menu.view": "Vis",
"menu.preferences": "Indstillinger",
"menu.sign.in": "Log ind",
"menu.sign.out": "Log ud",
"sponsored": "Sponsoreret",
"become.a.sponsor": "Bliv sponsor",
"zoom.to.selection": "Zoom til valgte",
"zoom.to.fit": "Zoom til lærred",
@ -87,4 +84,4 @@
"backward": "Tilbage",
"back": "Bagerst",
"language": "Sprog"
}
}

View file

@ -10,15 +10,12 @@
"zoom.in": "Heranzoomen",
"zoom.out": "Herauszoomen",
"to": "zu",
"to.selection": "Zur Auswahl",
"to.fit": "Anpassen",
"menu.file": "Datei",
"menu.edit": "Bearbeiten",
"menu.view": "Ansicht",
"menu.preferences": "Präferenzen",
"menu.sign.in": "Einloggen",
"menu.sign.out": "Ausloggen",
"sponsored": "Gesponsert",
"become.a.sponsor": "Sponsor werden",
"zoom.to.selection": "Zur Auswahl zoomen",
"zoom.to.fit": "Zoom anpassen",
@ -87,4 +84,4 @@
"backward": "Rückwärts",
"back": "Hinten",
"language": "Sprache"
}
}

View file

@ -10,15 +10,12 @@
"zoom.in": "Acercar",
"zoom.out": "Alejar",
"to": "A",
"to.selection": "A la selección",
"to.fit": "Ajustar",
"menu.file": "Archivo",
"menu.edit": "Editar",
"menu.view": "Ver",
"menu.preferences": "Preferencias",
"menu.sign.in": "Iniciar sesión",
"menu.sign.out": "Cerrar sesión",
"sponsored": "Patrocinado",
"become.a.sponsor": "Conviértete en patrocinador",
"zoom.to.content": "Acercar al contenido",
"zoom.to.selection": "Acercar a la selección",
@ -99,4 +96,4 @@
"top": "Arriba",
"search": "Buscar",
"page": "Página"
}
}

View file

@ -10,15 +10,12 @@
"zoom.in": "زوم جلو",
"zoom.out": "زوم عقب",
"to": "به",
"to.selection": "به انتخاب‌شده‌ها",
"to.fit": "به کل صفحه",
"menu.file": "فایل",
"menu.edit": "ویرایش",
"menu.view": "نمایش",
"menu.preferences": "تنظیم‌ها",
"menu.sign.in": "ورود",
"menu.sign.out": "خروج",
"sponsored": "حامیان",
"become.a.sponsor": " حامی شو",
"zoom.to.selection": "نمایش انتخاب‌شده‌ها",
"zoom.to.fit": "نمایش کل صفحه",
@ -87,4 +84,4 @@
"backward": "به عقب",
"back": "به آخر",
"language": "زبان"
}
}

View file

@ -10,8 +10,6 @@
"zoom.in": "Zoom avant",
"zoom.out": "Zoom arrière",
"to": "À",
"to.selection": "Sélection",
"to.fit": "Contenu",
"menu.tools": "Outils",
"menu.transform": "Transformation",
"menu.file": "Fichier",
@ -20,7 +18,6 @@
"menu.preferences": "Préférences",
"menu.sign.in": "S'authentifier",
"menu.sign.out": "Se déconnecter",
"sponsored": "Sponsorisé",
"become.a.sponsor": "Devenir un sponsor",
"zoom.to.content": "Retour au contenu",
"zoom.to.selection": "Ajuster le zoom à la sélection",
@ -99,4 +96,4 @@
"right": "À droite",
"top": "En haut",
"page": "Page"
}
}

View file

@ -10,15 +10,12 @@
"zoom.in": "הגדל תצוגה",
"zoom.out": "הקטן תצוגה",
"to": "ל",
"to.selection": "לסימון",
"to.fit": "להתאמה",
"menu.file": "קובץ",
"menu.edit": "עריכה",
"menu.view": "תצוגה",
"menu.preferences": "מאפיינים",
"menu.sign.in": "הירשם",
"menu.sign.out": "התנתק",
"sponsored": "חסות",
"become.a.sponsor": "מתן חסות",
"zoom.to.selection": "זום לבחירה",
"zoom.to.fit": "זום להתאמה",
@ -87,4 +84,4 @@
"backward": "אחורה",
"back": "בחזרה",
"language": "שפה"
}
}

View file

@ -10,15 +10,12 @@
"zoom.in": "Ingrandisci",
"zoom.out": "Rimpicciolisci",
"to": "Imposta",
"to.selection": "Adatta alla selezione",
"to.fit": "Adatta",
"menu.file": "File",
"menu.edit": "Modifica",
"menu.view": "Visualizzazione",
"menu.preferences": "Preferenze",
"menu.sign.in": "Accedi",
"menu.sign.out": "Esci",
"sponsored": "Sponsorizza",
"become.a.sponsor": "Sponsorizza",
"zoom.to.selection": "Adatta alla selezione",
"zoom.to.fit": "Adatta",
@ -93,4 +90,4 @@
"left": "Sinistra",
"right": "Destra",
"top": "In Alto"
}
}

View file

@ -10,15 +10,12 @@
"zoom.in": "拡大",
"zoom.out": "縮小",
"to": " ",
"to.selection": "選択したアイテムに合わせる",
"to.fit": "すべて表示",
"menu.file": "ファイル",
"menu.edit": "編集",
"menu.view": "表示",
"menu.preferences": "設定",
"menu.sign.in": "サインイン",
"menu.sign.out": "サインアウト",
"sponsored": "支援",
"become.a.sponsor": "支援する",
"zoom.to.selection": "選択したアイテムに合わせて拡大",
"zoom.to.fit": "拡大してすべてを表示",
@ -87,4 +84,4 @@
"backward": "ひとつ後ろへ",
"back": "最背面へ",
"language": "言語"
}
}

View file

@ -10,15 +10,12 @@
"zoom.in": "확대",
"zoom.out": "축소",
"to": "to",
"to.selection": "선택 요소 맞춤",
"to.fit": "전체 맞춤",
"menu.file": "파일",
"menu.edit": "편집",
"menu.view": "보기",
"menu.preferences": "설정",
"menu.sign.in": "로그인",
"menu.sign.out": "로그아웃",
"sponsored": "후원",
"become.a.sponsor": "후원자 되기",
"zoom.to.selection": "선택 요소 맞추기",
"zoom.to.fit": "전체 맞추기",
@ -87,4 +84,4 @@
"backward": "뒤로",
"back": "맨 뒤로",
"language": "언어"
}
}

View file

@ -10,8 +10,6 @@
"zoom.in": "Zoom In",
"zoom.out": "Zoom Out",
"to": "To",
"to.selection": "To Selection",
"to.fit": "To Fit",
"menu.tools": "Tools",
"menu.transform": "Transform",
"menu.file": "File",
@ -20,7 +18,6 @@
"menu.preferences": "Preferences",
"menu.sign.in": "Sign In",
"menu.sign.out": "Sign Out",
"sponsored": "Sponsored",
"become.a.sponsor": "Become a Sponsor",
"zoom.to.content": "Back to content",
"zoom.to.selection": "Zoom to Selection",
@ -105,5 +102,8 @@
"transparent": "Transparent",
"auto": "Auto",
"light": "Light",
"dark": "Dark"
}
"dark": "Dark",
"github": "Github",
"twitter": "Twitter",
"discord": "Discord"
}

View file

@ -10,15 +10,12 @@
"zoom.in": "जुम इन",
"zoom.out": "जुम आउट",
"to": "टु",
"to.selection": "टु सेलेक्सन",
"to.fit": "टु फिट",
"menu.file": "फाइल",
"menu.edit": "सम्पादन गर्नुहोस्",
"menu.view": "भ्यू",
"menu.preferences": "प्राथमिकताहरू",
"menu.sign.in": "साइन इन गर्नुहोस्",
"menu.sign.out": "साइन आउट गर्नुहोस्",
"sponsored": "प्रायोजित",
"become.a.sponsor": "प्रायोजक बन्नुहोस्",
"zoom.to.selection": "जुम टु सेलेक्सन",
"zoom.to.fit": "जुम टु फिट",
@ -87,4 +84,4 @@
"backward": "पछाडि",
"back": "थप पछाडि",
"language": "भाषा"
}
}

View file

@ -10,15 +10,12 @@
"zoom.in": "Zoom inn",
"zoom.out": "Zoom ut",
"to": "til",
"to.selection": "Til valg",
"to.fit": "For å passe",
"menu.file": "Fil",
"menu.edit": "Rediger",
"menu.view": "Vis",
"menu.preferences": "Preferanser",
"menu.sign.in": "Logg inn",
"menu.sign.out": "Logg ut",
"sponsored": "Sponset",
"become.a.sponsor": "Bli en sponsor",
"zoom.to.selection": "Zoom til valg",
"zoom.to.fit": "Zoom for å passe",
@ -87,4 +84,4 @@
"backward": "Bakover",
"back": "Bakerst",
"language": "Språk"
}
}

View file

@ -10,15 +10,12 @@
"zoom.in": "Przybliż",
"zoom.out": "Oddal",
"to": "do",
"to.selection": "Do zaznaczenia",
"to.fit": "Do wypełnienia",
"menu.file": "Plik",
"menu.edit": "Edycja",
"menu.view": "Widok",
"menu.preferences": "Preferencje",
"menu.sign.in": "Zaloguj",
"menu.sign.out": "Wyloguj",
"sponsored": "Sponsorzy",
"become.a.sponsor": "Zostań sponsorem",
"zoom.to.selection": "Przybliż do zaznaczenia",
"zoom.to.fit": "Wypełnij ekran",
@ -87,4 +84,4 @@
"backward": "Do tyłu",
"back": "Na spód",
"language": "Język"
}
}

View file

@ -10,15 +10,12 @@
"zoom.in": "Aumentar zoom",
"zoom.out": "Diminuir zoom",
"to": "para",
"to.selection": "Para seleção",
"to.fit": "Para encaixar",
"menu.file": "Arquivo",
"menu.edit": "Editar",
"menu.view": "Visualizar",
"menu.preferences": "Preferências",
"menu.sign.in": "Entrar",
"menu.sign.out": "Sair",
"sponsored": "Patrocinado",
"become.a.sponsor": "Torne-se um patrocinador",
"zoom.to.selection": "Zoom para a seleção",
"zoom.to.fit": "Zoom para ajuste",
@ -87,4 +84,4 @@
"backward": "Recuar",
"back": "Voltar",
"language": "Idioma"
}
}

View file

@ -10,15 +10,12 @@
"zoom.in": "Увеличить",
"zoom.out": "Уменьшить",
"to": "к",
"to.selection": "К выделению",
"to.fit": "По размеру экрана",
"menu.file": "Файл",
"menu.edit": "Редактирование",
"menu.view": "Вид",
"menu.preferences": "Настройки",
"menu.sign.in": "Войти",
"menu.sign.out": "Выйти",
"sponsored": "Спонсировано",
"become.a.sponsor": "Стать спонсором",
"zoom.to.selection": "Приблизить к выделению",
"zoom.to.fit": "Увеличить по размеру экрана",
@ -87,4 +84,4 @@
"backward": "На задний план",
"back": "Назад",
"language": "Язык"
}
}

View file

@ -10,15 +10,12 @@
"zoom.in": "Yakınlaştır",
"zoom.out": "Uzaklaştır",
"to": "",
"to.selection": "Seçime Göre",
"to.fit": "Sığdırmaya Göre",
"menu.file": "Dosya",
"menu.edit": "Düzenle",
"menu.view": "Görüntü",
"menu.preferences": "Tercihler",
"menu.sign.in": "Giriş Yap",
"menu.sign.out": "Oturumu Kapat",
"sponsored": "Sponsorlu",
"become.a.sponsor": "Sponsor Ol",
"zoom.to.selection": "Seçime Yakınlaştır",
"zoom.to.fit": "Sığdırmak için Yakınlaştır",
@ -87,4 +84,4 @@
"backward": "En Arkaya",
"back": "Arkaya",
"language": "Dil"
}
}

View file

@ -10,15 +10,12 @@
"zoom.in": "Збільшити",
"zoom.out": "Зменшити",
"to": "до",
"to.selection": "До виділення",
"to.fit": "За розміром екрану",
"menu.file": "Файл",
"menu.edit": "Редагування",
"menu.view": "Вигляд",
"menu.preferences": "Налаштування",
"menu.sign.in": "Увійти",
"menu.sign.out": "Вийти",
"sponsored": "Спонсовано",
"become.a.sponsor": "Стати спонсором",
"zoom.to.selection": "Наблизити до виділення",
"zoom.to.fit": "Збільшити за розміром екрану",
@ -87,4 +84,4 @@
"backward": "На задній план",
"back": "Назад",
"language": "Мова"
}
}

View file

@ -10,15 +10,12 @@
"zoom.in": "放大",
"zoom.out": "缩小",
"to": "缩放至",
"to.selection": "缩放选中",
"to.fit": "自适应缩放",
"menu.file": "文件",
"menu.edit": "编辑",
"menu.view": "视图",
"menu.preferences": "偏好",
"menu.sign.in": "登录",
"menu.sign.out": "登出",
"sponsored": "已赞助",
"become.a.sponsor": "成为赞助者",
"zoom.to.selection": "缩放选中",
"zoom.to.fit": "自适应缩放",
@ -87,4 +84,4 @@
"backward": "下一层",
"back": "置底",
"language": "语言"
}
}

View file

@ -10,15 +10,12 @@
"zoom.in": "放大",
"zoom.out": "縮小",
"to": "至",
"to.selection": "至選取範圍",
"to.fit": "至適當大小",
"menu.file": "檔案",
"menu.edit": "編輯",
"menu.view": "檢視",
"menu.preferences": "選項",
"menu.sign.in": "登入",
"menu.sign.out": "登出",
"sponsored": "贊助",
"become.a.sponsor": "成為贊助者",
"zoom.to.selection": "縮放至選取範圍",
"zoom.to.fit": "縮放至適當大小",
@ -95,4 +92,4 @@
"right": "右側",
"top": "上方",
"page": "頁面"
}
}