kopia lustrzana https://github.com/Tldraw/Tldraw
Various fixes
- Changes the brush color in dark mode - Fixes the paste button - Fixes a bug with pasting on mobile - Adds a Sign Out button to the menu - Hides the status bar in production - Adds debug mode to preferences - Refactors style panel - Hides keyboard shortcuts when not on mobilepull/44/head
rodzic
293edd7683
commit
74ff10bfd5
|
@ -15,6 +15,7 @@ import Coop from './coop/coop'
|
|||
import Brush from './brush'
|
||||
import Defs from './defs'
|
||||
import Page from './page'
|
||||
import useSafariFocusOutFix from 'hooks/useSafariFocusOutFix'
|
||||
|
||||
function resetError() {
|
||||
null
|
||||
|
@ -28,6 +29,8 @@ export default function Canvas(): JSX.Element {
|
|||
|
||||
useZoomEvents()
|
||||
|
||||
useSafariFocusOutFix()
|
||||
|
||||
const events = useCanvasEvents(rCanvas)
|
||||
|
||||
const isReady = useSelector((s) => s.isIn('ready'))
|
||||
|
@ -62,9 +65,10 @@ const MainSVG = styled('svg', {
|
|||
height: '100%',
|
||||
touchAction: 'none',
|
||||
zIndex: 100,
|
||||
backgroundColor: '$canvas',
|
||||
pointerEvents: 'all',
|
||||
// cursor: 'none',
|
||||
backgroundColor: '$canvas',
|
||||
borderTop: '1px solid $border',
|
||||
borderBottom: '1px solid $border',
|
||||
|
||||
'& *': {
|
||||
userSelect: 'none',
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
ContextMenuRoot,
|
||||
MenuContent,
|
||||
} from 'components/shared'
|
||||
import { commandKey, deepCompareArrays, isMobile } from 'utils'
|
||||
import { commandKey, deepCompareArrays } from 'utils'
|
||||
import state, { useSelector } from 'state'
|
||||
import {
|
||||
AlignType,
|
||||
|
@ -36,6 +36,7 @@ import {
|
|||
StretchHorizontallyIcon,
|
||||
StretchVerticallyIcon,
|
||||
} from '@radix-ui/react-icons'
|
||||
import { Kbd } from 'components/shared'
|
||||
|
||||
function alignTop() {
|
||||
state.send('ALIGNED', { type: AlignType.Top })
|
||||
|
@ -101,34 +102,30 @@ export default function ContextMenu({
|
|||
return (
|
||||
<ContextMenuRoot>
|
||||
<_ContextMenu.Trigger>{children}</_ContextMenu.Trigger>
|
||||
<MenuContent
|
||||
as={_ContextMenu.Content}
|
||||
ref={rContent}
|
||||
isMobile={isMobile()}
|
||||
>
|
||||
<MenuContent as={_ContextMenu.Content} ref={rContent}>
|
||||
{selectedShapeIds.length ? (
|
||||
<>
|
||||
{/* <ContextMenuButton onSelect={() => state.send('COPIED')}>
|
||||
<span>Copy</span>
|
||||
<kbd>
|
||||
<Kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>C</span>
|
||||
</kbd>
|
||||
</Kbd>
|
||||
</ContextMenuButton>
|
||||
<ContextMenuButton onSelect={() => state.send('CUT')}>
|
||||
<span>Cut</span>
|
||||
<kbd>
|
||||
<Kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>X</span>
|
||||
</kbd>
|
||||
</Kbd>
|
||||
</ContextMenuButton>
|
||||
*/}
|
||||
<ContextMenuButton onSelect={() => state.send('DUPLICATED')}>
|
||||
<span>Duplicate</span>
|
||||
<kbd>
|
||||
<Kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>D</span>
|
||||
</kbd>
|
||||
</Kbd>
|
||||
</ContextMenuButton>
|
||||
<ContextMenuDivider />
|
||||
{hasGroupSelected ||
|
||||
|
@ -137,20 +134,20 @@ export default function ContextMenu({
|
|||
{hasGroupSelected && (
|
||||
<ContextMenuButton onSelect={() => state.send('UNGROUPED')}>
|
||||
<span>Ungroup</span>
|
||||
<kbd>
|
||||
<Kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>⇧</span>
|
||||
<span>G</span>
|
||||
</kbd>
|
||||
</Kbd>
|
||||
</ContextMenuButton>
|
||||
)}
|
||||
{hasTwoOrMore && (
|
||||
<ContextMenuButton onSelect={() => state.send('GROUPED')}>
|
||||
<span>Group</span>
|
||||
<kbd>
|
||||
<Kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>G</span>
|
||||
</kbd>
|
||||
</Kbd>
|
||||
</ContextMenuButton>
|
||||
)}
|
||||
</>
|
||||
|
@ -164,11 +161,11 @@ export default function ContextMenu({
|
|||
}
|
||||
>
|
||||
<span>To Front</span>
|
||||
<kbd>
|
||||
<Kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>⇧</span>
|
||||
<span>]</span>
|
||||
</kbd>
|
||||
</Kbd>
|
||||
</ContextMenuButton>
|
||||
|
||||
<ContextMenuButton
|
||||
|
@ -179,10 +176,10 @@ export default function ContextMenu({
|
|||
}
|
||||
>
|
||||
<span>Forward</span>
|
||||
<kbd>
|
||||
<Kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>]</span>
|
||||
</kbd>
|
||||
</Kbd>
|
||||
</ContextMenuButton>
|
||||
<ContextMenuButton
|
||||
onSelect={() =>
|
||||
|
@ -192,10 +189,10 @@ export default function ContextMenu({
|
|||
}
|
||||
>
|
||||
<span>Backward</span>
|
||||
<kbd>
|
||||
<Kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>[</span>
|
||||
</kbd>
|
||||
</Kbd>
|
||||
</ContextMenuButton>
|
||||
<ContextMenuButton
|
||||
onSelect={() =>
|
||||
|
@ -205,11 +202,11 @@ export default function ContextMenu({
|
|||
}
|
||||
>
|
||||
<span>To Back</span>
|
||||
<kbd>
|
||||
<Kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>⇧</span>
|
||||
<span>[</span>
|
||||
</kbd>
|
||||
</Kbd>
|
||||
</ContextMenuButton>
|
||||
</ContextMenuSubMenu>
|
||||
{hasTwoOrMore && (
|
||||
|
@ -221,36 +218,36 @@ export default function ContextMenu({
|
|||
<MoveToPageMenu />
|
||||
<ContextMenuButton onSelect={() => state.send('COPIED_TO_SVG')}>
|
||||
<span>Copy to SVG</span>
|
||||
<kbd>
|
||||
<Kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>⇧</span>
|
||||
<span>C</span>
|
||||
</kbd>
|
||||
</Kbd>
|
||||
</ContextMenuButton>
|
||||
<ContextMenuDivider />
|
||||
<ContextMenuButton onSelect={() => state.send('DELETED')}>
|
||||
<span>Delete</span>
|
||||
<kbd>
|
||||
<Kbd>
|
||||
<span>⌫</span>
|
||||
</kbd>
|
||||
</Kbd>
|
||||
</ContextMenuButton>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ContextMenuButton onSelect={() => state.send('UNDO')}>
|
||||
<span>Undo</span>
|
||||
<kbd>
|
||||
<Kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>Z</span>
|
||||
</kbd>
|
||||
</Kbd>
|
||||
</ContextMenuButton>
|
||||
<ContextMenuButton onSelect={() => state.send('REDO')}>
|
||||
<span>Redo</span>
|
||||
<kbd>
|
||||
<Kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>⇧</span>
|
||||
<span>Z</span>
|
||||
</kbd>
|
||||
</Kbd>
|
||||
</ContextMenuButton>
|
||||
</>
|
||||
)}
|
||||
|
@ -277,7 +274,6 @@ function AlignDistributeSubMenu({
|
|||
as={_ContextMenu.Content}
|
||||
sideOffset={2}
|
||||
alignOffset={-2}
|
||||
isMobile={isMobile()}
|
||||
selectedStyle={hasThreeOrMore ? 'threeOrMore' : 'twoOrMore'}
|
||||
>
|
||||
<ContextMenuIconButton onSelect={alignLeft}>
|
||||
|
@ -355,12 +351,7 @@ function MoveToPageMenu() {
|
|||
<ChevronRightIcon />
|
||||
</IconWrapper>
|
||||
</ContextMenuButton>
|
||||
<MenuContent
|
||||
as={_ContextMenu.Content}
|
||||
sideOffset={2}
|
||||
alignOffset={-2}
|
||||
isMobile={isMobile()}
|
||||
>
|
||||
<MenuContent as={_ContextMenu.Content} sideOffset={2} alignOffset={-2}>
|
||||
{sorted.map(({ id, name }) => (
|
||||
<ContextMenuButton
|
||||
key={id}
|
||||
|
|
|
@ -2,14 +2,9 @@ import { useSelector } from 'state'
|
|||
import { ShapeTreeNode } from 'types'
|
||||
import ShapeComponent from './shape'
|
||||
|
||||
/*
|
||||
On each state change, populate a tree structure with all of
|
||||
the shapes that we need to render..
|
||||
*/
|
||||
|
||||
export default function Page(): JSX.Element {
|
||||
// Get a tree of shapes to render
|
||||
const shapesToRender = useSelector((s) => s.values.shapesToRender)
|
||||
|
||||
const allowHovers = useSelector((s) =>
|
||||
s.isInAny('selecting', 'text', 'editingShape')
|
||||
)
|
||||
|
|
|
@ -2,8 +2,13 @@
|
|||
import styled from 'styles'
|
||||
import React, { useRef } from 'react'
|
||||
import state, { useSelector } from 'state'
|
||||
import * as Panel from '../panel'
|
||||
import { breakpoints, IconButton, RowButton, IconWrapper } from '../shared'
|
||||
import * as Panel from 'components/panel'
|
||||
import {
|
||||
breakpoints,
|
||||
IconButton,
|
||||
RowButton,
|
||||
IconWrapper,
|
||||
} from 'components/shared'
|
||||
import {
|
||||
Cross2Icon,
|
||||
PlayIcon,
|
||||
|
@ -179,7 +184,6 @@ export default function CodePanel(): JSX.Element {
|
|||
}
|
||||
|
||||
const StylePanelRoot = styled(Panel.Root, {
|
||||
marginRight: '8px',
|
||||
width: 'fit-content',
|
||||
maxWidth: 'fit-content',
|
||||
overflow: 'hidden',
|
||||
|
|
|
@ -2,7 +2,6 @@ import useKeyboardEvents from 'hooks/useKeyboardEvents'
|
|||
import useLoadOnMount from 'hooks/useLoadOnMount'
|
||||
import Menu from './menu/menu'
|
||||
import Canvas from './canvas/canvas'
|
||||
import StatusBar from './status-bar'
|
||||
import ToolsPanel from './tools-panel/tools-panel'
|
||||
import StylePanel from './style-panel/style-panel'
|
||||
import styled from 'styles'
|
||||
|
@ -28,7 +27,6 @@ export default function Editor({ roomId }: { roomId?: string }): JSX.Element {
|
|||
<StylePanel />
|
||||
<Canvas />
|
||||
<ToolsPanel />
|
||||
<StatusBar />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
@ -56,6 +54,8 @@ const Layout = styled('main', {
|
|||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'flex-start',
|
||||
boxSizing: 'border-box',
|
||||
|
||||
pointerEvents: 'none',
|
||||
'& > *': {
|
||||
PointerEvent: 'all',
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from 'react'
|
||||
import { HamburgerMenuIcon } from '@radix-ui/react-icons'
|
||||
import { ExitIcon, HamburgerMenuIcon } from '@radix-ui/react-icons'
|
||||
import { Trigger, Content } from '@radix-ui/react-dropdown-menu'
|
||||
import { memo } from 'react'
|
||||
import {
|
||||
|
@ -12,14 +12,18 @@ import {
|
|||
DropdownMenuSubMenu,
|
||||
DropdownMenuDivider,
|
||||
DropdownMenuCheckboxItem,
|
||||
IconWrapper,
|
||||
Kbd,
|
||||
} from '../shared'
|
||||
import state, { useSelector } from 'state'
|
||||
import { commandKey } from 'utils'
|
||||
import { signOut } from 'next-auth/client'
|
||||
|
||||
const handleNew = () => state.send('CREATED_NEW_PROJECT')
|
||||
const handleSave = () => state.send('SAVED')
|
||||
const handleLoad = () => state.send('LOADED_FROM_FILE_STSTEM')
|
||||
const toggleDarkMode = () => state.send('TOGGLED_DARK_MODE')
|
||||
const toggleDebugMode = () => state.send('TOGGLED_DEBUG_MODE')
|
||||
|
||||
function Menu() {
|
||||
return (
|
||||
|
@ -31,38 +35,45 @@ function Menu() {
|
|||
<Content as={MenuContent} sideOffset={8}>
|
||||
<DropdownMenuButton onSelect={handleNew} disabled>
|
||||
<span>New Project</span>
|
||||
<kbd>
|
||||
<Kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>N</span>
|
||||
</kbd>
|
||||
</Kbd>
|
||||
</DropdownMenuButton>
|
||||
<DropdownMenuDivider />
|
||||
<DropdownMenuButton onSelect={handleLoad}>
|
||||
<span>Open...</span>
|
||||
<kbd>
|
||||
<Kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>L</span>
|
||||
</kbd>
|
||||
</Kbd>
|
||||
</DropdownMenuButton>
|
||||
<RecentFiles />
|
||||
<DropdownMenuDivider />
|
||||
<DropdownMenuButton onSelect={handleSave}>
|
||||
<span>Save</span>
|
||||
<kbd>
|
||||
<Kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>S</span>
|
||||
</kbd>
|
||||
</Kbd>
|
||||
</DropdownMenuButton>
|
||||
<DropdownMenuButton onSelect={handleSave}>
|
||||
<span>Save As...</span>
|
||||
<kbd>
|
||||
<Kbd>
|
||||
<span>⇧</span>
|
||||
<span>{commandKey()}</span>
|
||||
<span>S</span>
|
||||
</kbd>
|
||||
</Kbd>
|
||||
</DropdownMenuButton>
|
||||
<DropdownMenuDivider />
|
||||
<Preferences />
|
||||
<DropdownMenuDivider />
|
||||
<DropdownMenuButton onSelect={signOut}>
|
||||
<span>Sign Out</span>
|
||||
<IconWrapper size="small">
|
||||
<ExitIcon />
|
||||
</IconWrapper>
|
||||
</DropdownMenuButton>
|
||||
</Content>
|
||||
</DropdownMenuRoot>
|
||||
</FloatingContainer>
|
||||
|
@ -88,6 +99,7 @@ function RecentFiles() {
|
|||
}
|
||||
|
||||
function Preferences() {
|
||||
const isDebugMode = useSelector((s) => s.data.settings.isDebugMode)
|
||||
const isDarkMode = useSelector((s) => s.data.settings.isDarkMode)
|
||||
|
||||
return (
|
||||
|
@ -98,6 +110,12 @@ function Preferences() {
|
|||
>
|
||||
<span>Dark Mode</span>
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={isDebugMode}
|
||||
onCheckedChange={toggleDebugMode}
|
||||
>
|
||||
<span>Debug Mode</span>
|
||||
</DropdownMenuCheckboxItem>
|
||||
</DropdownMenuSubMenu>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
|||
import * as RadioGroup from '@radix-ui/react-radio-group'
|
||||
import * as Panel from './panel'
|
||||
import styled from 'styles'
|
||||
import { forwardRef } from 'react'
|
||||
import React, { forwardRef } from 'react'
|
||||
import { CheckIcon, ChevronRightIcon } from '@radix-ui/react-icons'
|
||||
import { isMobile } from 'utils'
|
||||
|
||||
|
@ -466,6 +466,14 @@ export const FloatingContainer = styled('div', {
|
|||
zIndex: 200,
|
||||
|
||||
variants: {
|
||||
direction: {
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
column: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
},
|
||||
elevation: {
|
||||
0: {
|
||||
boxShadow: 'none',
|
||||
|
@ -483,6 +491,23 @@ export const FloatingContainer = styled('div', {
|
|||
},
|
||||
})
|
||||
|
||||
export const StyledKbd = styled('kbd', {
|
||||
marginLeft: '32px',
|
||||
fontSize: '$1',
|
||||
fontFamily: '$ui',
|
||||
fontWeight: 400,
|
||||
|
||||
'& > span': {
|
||||
display: 'inline-block',
|
||||
width: '12px',
|
||||
},
|
||||
})
|
||||
|
||||
export function Kbd({ children }: { children: React.ReactNode }): JSX.Element {
|
||||
if (isMobile()) return null
|
||||
return <StyledKbd>{children}</StyledKbd>
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Menus */
|
||||
/* -------------------------------------------------- */
|
||||
|
@ -498,30 +523,17 @@ export const MenuContent = styled('div', {
|
|||
border: '1px solid $panel',
|
||||
padding: '$0',
|
||||
boxShadow: '$4',
|
||||
minWidth: 200,
|
||||
minWidth: 180,
|
||||
font: '$ui',
|
||||
})
|
||||
|
||||
'& kbd': {
|
||||
marginLeft: '32px',
|
||||
fontSize: '$1',
|
||||
fontFamily: '$ui',
|
||||
fontWeight: 400,
|
||||
},
|
||||
|
||||
'& kbd > span': {
|
||||
display: 'inline-block',
|
||||
width: '12px',
|
||||
},
|
||||
|
||||
variants: {
|
||||
isMobile: {
|
||||
true: {
|
||||
'& kbd': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
export const Divider = styled('div', {
|
||||
backgroundColor: '$hover',
|
||||
height: 1,
|
||||
marginTop: '$2',
|
||||
marginRight: '-$2',
|
||||
marginBottom: '$2',
|
||||
marginLeft: '-$2',
|
||||
})
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
|
@ -565,12 +577,7 @@ export function DropdownMenuSubMenu({
|
|||
<ChevronRightIcon />
|
||||
</IconWrapper>
|
||||
</RowButton>
|
||||
<MenuContent
|
||||
as={DropdownMenu.Content}
|
||||
sideOffset={2}
|
||||
alignOffset={-2}
|
||||
isMobile={isMobile()}
|
||||
>
|
||||
<MenuContent as={DropdownMenu.Content} sideOffset={2} alignOffset={-2}>
|
||||
{children}
|
||||
<DropdownMenuArrow offset={13} />
|
||||
</MenuContent>
|
||||
|
@ -699,12 +706,7 @@ export function ContextMenuSubMenu({
|
|||
<ChevronRightIcon />
|
||||
</IconWrapper>
|
||||
</ContextMenu.TriggerItem>
|
||||
<ContextMenu.Content
|
||||
as={MenuContent}
|
||||
sideOffset={2}
|
||||
alignOffset={-2}
|
||||
isMobile={isMobile()}
|
||||
>
|
||||
<ContextMenu.Content as={MenuContent} sideOffset={2} alignOffset={-2}>
|
||||
{children}
|
||||
<ContextMenuArrow offset={13} />
|
||||
</ContextMenu.Content>
|
||||
|
|
|
@ -17,6 +17,8 @@ export default function StatusBar(): JSX.Element {
|
|||
|
||||
const log = local.log[0]
|
||||
|
||||
if (process.env.NODE_ENV === 'development') return null
|
||||
|
||||
return (
|
||||
<StatusBarContainer size={size}>
|
||||
<Section>
|
||||
|
@ -28,11 +30,6 @@ export default function StatusBar(): JSX.Element {
|
|||
}
|
||||
|
||||
const StatusBarContainer = styled('div', {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
zIndex: 300,
|
||||
height: 40,
|
||||
userSelect: 'none',
|
||||
borderTop: '1px solid $border',
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
import styled from 'styles'
|
||||
import state, { useSelector } from 'state'
|
||||
import * as Panel from 'components/panel'
|
||||
import { useRef } from 'react'
|
||||
import {
|
||||
IconButton,
|
||||
IconWrapper,
|
||||
ButtonsRow,
|
||||
RowButton,
|
||||
breakpoints,
|
||||
RowButton,
|
||||
FloatingContainer,
|
||||
Divider,
|
||||
Kbd,
|
||||
} from 'components/shared'
|
||||
import ShapesFunctions from './shapes-functions'
|
||||
import AlignDistribute from './align-distribute'
|
||||
|
@ -16,14 +15,8 @@ import QuickSizeSelect from './quick-size-select'
|
|||
import QuickDashSelect from './quick-dash-select'
|
||||
import QuickFillSelect from './quick-fill-select'
|
||||
import Tooltip from 'components/tooltip'
|
||||
import { motion } from 'framer-motion'
|
||||
import {
|
||||
ClipboardCopyIcon,
|
||||
ClipboardIcon,
|
||||
DotsHorizontalIcon,
|
||||
Share2Icon,
|
||||
Cross2Icon,
|
||||
} from '@radix-ui/react-icons'
|
||||
import { DotsHorizontalIcon, Cross2Icon } from '@radix-ui/react-icons'
|
||||
import { commandKey, isMobile } from 'utils'
|
||||
|
||||
const handleStylePanelOpen = () => state.send('TOGGLED_STYLE_PANEL_OPEN')
|
||||
const handleCopy = () => state.send('COPIED')
|
||||
|
@ -31,12 +24,10 @@ const handlePaste = () => state.send('PASTED')
|
|||
const handleCopyToSvg = () => state.send('COPIED_TO_SVG')
|
||||
|
||||
export default function StylePanel(): JSX.Element {
|
||||
const rContainer = useRef<HTMLDivElement>(null)
|
||||
|
||||
const isOpen = useSelector((s) => s.data.settings.isStyleOpen)
|
||||
|
||||
return (
|
||||
<StylePanelRoot dir="ltr" ref={rContainer} isOpen={isOpen}>
|
||||
<FloatingContainer direction="column">
|
||||
<ButtonsRow>
|
||||
<QuickColorSelect />
|
||||
<QuickSizeSelect />
|
||||
|
@ -54,84 +45,57 @@ export default function StylePanel(): JSX.Element {
|
|||
</IconButton>
|
||||
</ButtonsRow>
|
||||
{isOpen && <SelectedShapeContent />}
|
||||
</StylePanelRoot>
|
||||
</FloatingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectedShapeContent(): JSX.Element {
|
||||
const selectedShapesCount = useSelector((s) => s.values.selectedIds.length)
|
||||
|
||||
const showKbds = !isMobile()
|
||||
|
||||
return (
|
||||
<>
|
||||
<hr />
|
||||
<Divider />
|
||||
<ShapesFunctions />
|
||||
<hr />
|
||||
<Divider />
|
||||
<AlignDistribute
|
||||
hasTwoOrMore={selectedShapesCount > 1}
|
||||
hasThreeOrMore={selectedShapesCount > 2}
|
||||
/>
|
||||
<hr />
|
||||
<Divider />
|
||||
<RowButton
|
||||
bp={breakpoints}
|
||||
disabled={selectedShapesCount === 0}
|
||||
onClick={handleCopy}
|
||||
>
|
||||
<span>Copy</span>
|
||||
<IconWrapper size="small">
|
||||
<ClipboardCopyIcon />
|
||||
</IconWrapper>
|
||||
{showKbds && (
|
||||
<Kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>C</span>
|
||||
</Kbd>
|
||||
)}
|
||||
</RowButton>
|
||||
<RowButton bp={breakpoints} onClick={handlePaste}>
|
||||
<span>Paste</span>
|
||||
<IconWrapper size="small">
|
||||
<ClipboardIcon />
|
||||
</IconWrapper>
|
||||
{showKbds && (
|
||||
<Kbd>
|
||||
<span>{commandKey()}</span>
|
||||
<span>V</span>
|
||||
</Kbd>
|
||||
)}
|
||||
</RowButton>
|
||||
<RowButton
|
||||
bp={breakpoints}
|
||||
disabled={selectedShapesCount === 0}
|
||||
onClick={handleCopyToSvg}
|
||||
>
|
||||
<RowButton bp={breakpoints} onClick={handleCopyToSvg}>
|
||||
<span>Copy to SVG</span>
|
||||
<IconWrapper size="small">
|
||||
<Share2Icon />
|
||||
</IconWrapper>
|
||||
{showKbds && (
|
||||
<Kbd>
|
||||
<span>⇧</span>
|
||||
<span>{commandKey()}</span>
|
||||
<span>C</span>
|
||||
</Kbd>
|
||||
)}
|
||||
</RowButton>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const StylePanelRoot = styled(motion(Panel.Root), {
|
||||
minWidth: 1,
|
||||
width: 'fit-content',
|
||||
maxWidth: 'fit-content',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
border: '1px solid $panel',
|
||||
boxShadow: '$4',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
pointerEvents: 'all',
|
||||
padding: '$0',
|
||||
zIndex: 300,
|
||||
|
||||
'& hr': {
|
||||
marginTop: 2,
|
||||
marginBottom: 2,
|
||||
marginLeft: '-$0',
|
||||
border: 'none',
|
||||
height: 1,
|
||||
backgroundColor: '$brushFill',
|
||||
width: 'calc(100% + 4px)',
|
||||
},
|
||||
|
||||
variants: {
|
||||
isOpen: {
|
||||
true: {},
|
||||
false: {
|
||||
width: 'fit-content',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -8,10 +8,11 @@ import {
|
|||
SquareIcon,
|
||||
TextIcon,
|
||||
} from '@radix-ui/react-icons'
|
||||
import { PrimaryButton, SecondaryButton } from './shared'
|
||||
import { FloatingContainer } from '../shared'
|
||||
import React from 'react'
|
||||
import * as React from 'react'
|
||||
import state, { useSelector } from 'state'
|
||||
import StatusBar from 'components/status-bar'
|
||||
import { FloatingContainer } from 'components/shared'
|
||||
import { PrimaryButton, SecondaryButton } from './shared'
|
||||
import styled from 'styles'
|
||||
import { ShapeType } from 'types'
|
||||
import UndoRedo from './undo-redo'
|
||||
|
@ -97,13 +98,16 @@ export default function ToolsPanel(): JSX.Element {
|
|||
</FloatingContainer>
|
||||
<UndoRedo />
|
||||
</RightWrap>
|
||||
<StatusWrap>
|
||||
<StatusBar />
|
||||
</StatusWrap>
|
||||
</ToolsPanelContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const ToolsPanelContainer = styled('div', {
|
||||
position: 'fixed',
|
||||
bottom: 44,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
width: '100%',
|
||||
|
@ -111,10 +115,11 @@ const ToolsPanelContainer = styled('div', {
|
|||
maxWidth: '100%',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr auto 1fr',
|
||||
padding: '0 8px 12px 8px',
|
||||
padding: '0',
|
||||
alignItems: 'flex-end',
|
||||
zIndex: 200,
|
||||
gap: 12,
|
||||
gridGap: '$4',
|
||||
gridRowGap: '$4',
|
||||
})
|
||||
|
||||
const CenterWrap = styled('div', {
|
||||
|
@ -132,6 +137,7 @@ const LeftWrap = styled('div', {
|
|||
gridRow: 1,
|
||||
gridColumn: 1,
|
||||
display: 'flex',
|
||||
paddingLeft: '$3',
|
||||
variants: {
|
||||
size: {
|
||||
mobile: {
|
||||
|
@ -158,6 +164,7 @@ const RightWrap = styled('div', {
|
|||
gridRow: 1,
|
||||
gridColumn: 3,
|
||||
display: 'flex',
|
||||
paddingRight: '$3',
|
||||
variants: {
|
||||
size: {
|
||||
mobile: {
|
||||
|
@ -179,3 +186,8 @@ const RightWrap = styled('div', {
|
|||
},
|
||||
},
|
||||
})
|
||||
|
||||
const StatusWrap = styled('div', {
|
||||
gridRow: 2,
|
||||
gridColumn: '1 / span 3',
|
||||
})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
import { MutableRefObject, useCallback, useEffect } from 'react'
|
||||
import { MutableRefObject, useCallback } from 'react'
|
||||
import state from 'state'
|
||||
import {
|
||||
fastBrushSelect,
|
||||
|
@ -9,87 +9,80 @@ import {
|
|||
fastTranslate,
|
||||
} from 'state/hacks'
|
||||
import inputs from 'state/inputs'
|
||||
import { isMobile } from 'utils'
|
||||
import Vec from 'utils/vec'
|
||||
|
||||
function handleFocusOut() {
|
||||
state.send('BLURRED_EDITING_SHAPE')
|
||||
}
|
||||
|
||||
export default function useCanvasEvents(
|
||||
rCanvas: MutableRefObject<SVGGElement>
|
||||
) {
|
||||
const handlePointerDown = useCallback((e: React.PointerEvent) => {
|
||||
if (!inputs.canAccept(e.pointerId)) return
|
||||
const handlePointerDown = useCallback(
|
||||
(e: React.PointerEvent<SVGSVGElement>) => {
|
||||
if (!inputs.canAccept(e.pointerId)) return
|
||||
|
||||
rCanvas.current.setPointerCapture(e.pointerId)
|
||||
rCanvas.current.setPointerCapture(e.pointerId)
|
||||
|
||||
const info = inputs.pointerDown(e, 'canvas')
|
||||
const info = inputs.pointerDown(e, 'canvas')
|
||||
|
||||
if (e.button === 0) {
|
||||
if (inputs.isDoubleClick() && !(info.altKey || info.metaKey)) {
|
||||
state.send('DOUBLE_POINTED_CANVAS', info)
|
||||
if (e.button === 0) {
|
||||
if (inputs.isDoubleClick() && !(info.altKey || info.metaKey)) {
|
||||
state.send('DOUBLE_POINTED_CANVAS', info)
|
||||
}
|
||||
|
||||
state.send('POINTED_CANVAS', info)
|
||||
} else if (e.button === 2) {
|
||||
state.send('RIGHT_POINTED', info)
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const handlePointerMove = useCallback(
|
||||
(e: React.PointerEvent<SVGSVGElement>) => {
|
||||
if (!inputs.canAccept(e.pointerId)) return
|
||||
|
||||
const prev = inputs.pointer?.point
|
||||
const info = inputs.pointerMove(e)
|
||||
|
||||
if (prev && state.isIn('selecting') && inputs.keys[' ']) {
|
||||
const delta = Vec.sub(prev, info.point)
|
||||
fastPanUpdate(delta)
|
||||
state.send('KEYBOARD_PANNED_CAMERA', {
|
||||
delta: Vec.sub(prev, info.point),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
state.send('POINTED_CANVAS', info)
|
||||
} else if (e.button === 2) {
|
||||
state.send('RIGHT_POINTED', info)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handlePointerMove = useCallback((e: React.PointerEvent) => {
|
||||
if (!inputs.canAccept(e.pointerId)) return
|
||||
|
||||
const prev = inputs.pointer?.point
|
||||
const info = inputs.pointerMove(e)
|
||||
|
||||
if (prev && state.isIn('selecting') && inputs.keys[' ']) {
|
||||
const delta = Vec.sub(prev, info.point)
|
||||
fastPanUpdate(delta)
|
||||
state.send('KEYBOARD_PANNED_CAMERA', { delta: Vec.sub(prev, info.point) })
|
||||
return
|
||||
}
|
||||
|
||||
if (state.isIn('draw.editing')) {
|
||||
fastDrawUpdate(info)
|
||||
} else if (state.isIn('brushSelecting')) {
|
||||
fastBrushSelect(info.point)
|
||||
} else if (state.isIn('translatingSelection')) {
|
||||
fastTranslate(info)
|
||||
} else if (state.isIn('transformingSelection')) {
|
||||
fastTransform(info)
|
||||
}
|
||||
|
||||
state.send('MOVED_POINTER', info)
|
||||
}, [])
|
||||
|
||||
const handlePointerUp = useCallback((e: React.PointerEvent) => {
|
||||
if (!inputs.canAccept(e.pointerId)) return
|
||||
|
||||
rCanvas.current.releasePointerCapture(e.pointerId)
|
||||
|
||||
state.send('STOPPED_POINTING', {
|
||||
id: 'canvas',
|
||||
...inputs.pointerUp(e, 'canvas'),
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleTouchStart = useCallback(() => {
|
||||
// if (isMobile()) {
|
||||
// if (e.touches.length === 2) {
|
||||
// state.send('TOUCH_UNDO')
|
||||
// } else state.send('TOUCHED_CANVAS')
|
||||
// }
|
||||
}, [])
|
||||
|
||||
// Send event on iOS when a user presses the "Done" key while editing a text element
|
||||
useEffect(() => {
|
||||
if (isMobile()) {
|
||||
document.addEventListener('focusout', handleFocusOut)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('focusout', handleFocusOut)
|
||||
if (state.isIn('draw.editing')) {
|
||||
fastDrawUpdate(info)
|
||||
} else if (state.isIn('brushSelecting')) {
|
||||
fastBrushSelect(info.point)
|
||||
} else if (state.isIn('translatingSelection')) {
|
||||
fastTranslate(info)
|
||||
} else if (state.isIn('transformingSelection')) {
|
||||
fastTransform(info)
|
||||
}
|
||||
|
||||
state.send('MOVED_POINTER', info)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const handlePointerUp = useCallback(
|
||||
(e: React.PointerEvent<SVGSVGElement>) => {
|
||||
if (!inputs.canAccept(e.pointerId)) return
|
||||
|
||||
rCanvas.current.releasePointerCapture(e.pointerId)
|
||||
|
||||
state.send('STOPPED_POINTING', {
|
||||
id: 'canvas',
|
||||
...inputs.pointerUp(e, 'canvas'),
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const handleTouchStart = useCallback((e: React.TouchEvent<SVGSVGElement>) => {
|
||||
if ('safari' in window) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
|
|
@ -195,11 +195,7 @@ export default function useKeyboardEvents() {
|
|||
}
|
||||
case 'd': {
|
||||
if (metaKey(e)) {
|
||||
if (e.shiftKey) {
|
||||
state.send('TOGGLED_DEBUG_MODE')
|
||||
} else {
|
||||
state.send('DUPLICATED', info)
|
||||
}
|
||||
state.send('DUPLICATED', info)
|
||||
} else {
|
||||
state.send('SELECTED_DRAW_TOOL', info)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import isMobile from 'ismobilejs'
|
||||
import { useEffect } from 'react'
|
||||
import state from 'state'
|
||||
|
||||
// Send event on iOS when a user presses the "Done" key while editing
|
||||
// a text element.
|
||||
|
||||
function handleFocusOut() {
|
||||
state.send('BLURRED_EDITING_SHAPE')
|
||||
}
|
||||
|
||||
export default function useSafariFocusOutFix(): void {
|
||||
useEffect(() => {
|
||||
if (isMobile().apple) {
|
||||
document.addEventListener('focusout', handleFocusOut)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('focusout', handleFocusOut)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
}
|
|
@ -9,6 +9,8 @@ class Clipboard {
|
|||
fallback = false
|
||||
|
||||
copy = (shapes: Shape[], onComplete?: () => void) => {
|
||||
if (shapes === undefined) return
|
||||
|
||||
this.current = JSON.stringify({ id: 'tldr', shapes })
|
||||
|
||||
if ('permissions' in navigator && 'clipboard' in navigator) {
|
||||
|
@ -37,7 +39,7 @@ class Clipboard {
|
|||
return this
|
||||
}
|
||||
|
||||
sendPastedTextToState(text = this.current) {
|
||||
sendPastedTextToState = (text = this.current) => {
|
||||
if (text === undefined) return
|
||||
|
||||
try {
|
||||
|
@ -62,10 +64,13 @@ class Clipboard {
|
|||
|
||||
copySelectionToSvg(data: Data) {
|
||||
const shapes = tld.getSelectedShapes(data)
|
||||
const shapesToCopy = shapes.length > 0 ? shapes : tld.getShapes(data)
|
||||
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
||||
|
||||
shapes
|
||||
if (shapesToCopy.length === 0) return
|
||||
|
||||
shapesToCopy
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.forEach((shape) => {
|
||||
const group = document.getElementById(shape.id)
|
||||
|
@ -78,7 +83,7 @@ class Clipboard {
|
|||
})
|
||||
|
||||
const bounds = getCommonBounds(
|
||||
...shapes.map((shape) => getShapeUtils(shape).getBounds(shape))
|
||||
...shapesToCopy.map((shape) => getShapeUtils(shape).getBounds(shape))
|
||||
)
|
||||
|
||||
// No content
|
||||
|
|
|
@ -2137,8 +2137,13 @@ const state = createState({
|
|||
|
||||
pasteFromClipboard(data) {
|
||||
clipboard.paste()
|
||||
|
||||
if (clipboard.fallback) {
|
||||
commands.paste(data, JSON.parse(clipboard.current).shapes)
|
||||
try {
|
||||
commands.paste(data, JSON.parse(clipboard.current).shapes)
|
||||
} catch (e) {
|
||||
console.warn('Could not paste that text.')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -2245,16 +2250,15 @@ const state = createState({
|
|||
|
||||
return commonStyle
|
||||
},
|
||||
|
||||
shapesToRender(data) {
|
||||
const viewport = tld.getViewport(data)
|
||||
|
||||
const page = tld.getPage(data)
|
||||
|
||||
const currentShapes = Object.values(page.shapes)
|
||||
.filter((shape) => shape.parentId === page.id)
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
const shapesToShow = Object.values(page.shapes).filter((shape) => {
|
||||
if (shape.parentId !== page.id) return false
|
||||
|
||||
const shapesToShow = currentShapes.filter((shape) => {
|
||||
const shapeBounds = getShapeUtils(shape).getBounds(shape)
|
||||
|
||||
return (
|
||||
|
@ -2270,9 +2274,9 @@ const state = createState({
|
|||
|
||||
const selectedIds = tld.getSelectedIds(data)
|
||||
|
||||
shapesToShow.forEach((shape) =>
|
||||
tld.addToShapeTree(data, selectedIds, tree, shape)
|
||||
)
|
||||
shapesToShow
|
||||
.sort((a, b) => a.childIndex - b.childIndex)
|
||||
.forEach((shape) => tld.addToShapeTree(data, selectedIds, tree, shape))
|
||||
|
||||
return tree
|
||||
},
|
||||
|
|
|
@ -39,6 +39,7 @@ const { styled, global, css, theme, getCssString } = createCss({
|
|||
1: '3px',
|
||||
2: '4px',
|
||||
3: '8px',
|
||||
4: '12px',
|
||||
},
|
||||
fontSizes: {
|
||||
0: '10px',
|
||||
|
@ -96,8 +97,8 @@ const light = theme({})
|
|||
|
||||
const dark = theme({
|
||||
colors: {
|
||||
brushFill: 'rgba(0,0,0,.05)',
|
||||
brushStroke: 'rgba(0,0,0,.25)',
|
||||
brushFill: 'rgba(180, 180, 180, .05)',
|
||||
brushStroke: 'rgba(180, 180, 180, .25)',
|
||||
hint: 'rgba(216, 226, 249, 1.000)',
|
||||
selected: 'rgba(38, 150, 255, 1.000)',
|
||||
bounds: 'rgba(38, 150, 255, 1.000)',
|
||||
|
@ -136,6 +137,7 @@ const globalStyles = global({
|
|||
padding: '0px',
|
||||
margin: '0px',
|
||||
overscrollBehavior: 'none',
|
||||
overscrollBehaviorX: 'none',
|
||||
fontFamily: '$ui',
|
||||
fontSize: '$2',
|
||||
color: '$text',
|
||||
|
|
Ładowanie…
Reference in New Issue