[improvement] UI (#215)
* move folders out of packages * Remove custom yarn stuff, remove duplicate readme * Remove stitches config * Add README script. * bump deps * Fix script * Update package.json * rehauls UI * further rehauls UI * UI polish * Update ToolButton.tsx * Update ToolButton.tsx * Bump license * move tldraw to root * Remove SW
This commit is contained in:
parent
b68a4681e1
commit
e2369003c6
139 changed files with 2524 additions and 3066 deletions
21
packages/tldraw/LICENSE.md
Normal file
21
packages/tldraw/LICENSE.md
Normal file
|
@ -0,0 +1,21 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2021 Stephen Ruiz Ltd
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
BIN
packages/tldraw/card-repo.png
Normal file
BIN
packages/tldraw/card-repo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 63 KiB |
|
@ -46,7 +46,7 @@
|
|||
"@radix-ui/react-id": "^0.1.1",
|
||||
"@radix-ui/react-radio-group": "^0.1.1",
|
||||
"@radix-ui/react-tooltip": "^0.1.1",
|
||||
"@stitches/core": "^1.2.5",
|
||||
"@stitches/react": "^1.2.5",
|
||||
"@tldraw/core": "^0.1.13",
|
||||
"@tldraw/intersect": "^0.1.3",
|
||||
"@tldraw/vec": "^0.1.3",
|
||||
|
@ -55,4 +55,4 @@
|
|||
"rko": "^0.5.25"
|
||||
},
|
||||
"gitHead": "083b36e167b6911927a6b58cbbb830b11b33f00a"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
/* eslint-disable */
|
||||
const fs = require('fs')
|
||||
|
||||
const filesToCopy = ['README.md', 'card-repo.png']
|
||||
const filesToCopy = ['README.md', 'LICENSE.md', 'card-repo.png']
|
||||
|
||||
filesToCopy.forEach((file) => {
|
||||
fs.copyFile(`../../${file}`, `./dist/${file}`, (err) => {
|
||||
fs.copyFile(`../../${file}`, `./${file}`, (err) => {
|
||||
if (err) throw err
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import { TLDraw } from './tldraw'
|
||||
import { TLDraw } from './TLDraw'
|
||||
|
||||
describe('tldraw', () => {
|
||||
test('mounts component without crashing', () => {
|
|
@ -1,19 +1,16 @@
|
|||
import * as React from 'react'
|
||||
import { IdProvider } from '@radix-ui/react-id'
|
||||
import { Renderer } from '@tldraw/core'
|
||||
import css, { dark } from '~styles'
|
||||
import styled, { dark } from '~styles'
|
||||
import { Data, TLDrawDocument, TLDrawStatus, TLDrawUser } from '~types'
|
||||
import { TLDrawState } from '~state'
|
||||
import { TLDrawContext, useCustomFonts, useKeyboardShortcuts, useTLDrawContext } from '~hooks'
|
||||
import { shapeUtils } from '~shape-utils'
|
||||
import { StylePanel } from '~components/style-panel'
|
||||
import { ToolsPanel } from '~components/tools-panel'
|
||||
import { PagePanel } from '~components/page-panel'
|
||||
import { Menu } from '~components/menu'
|
||||
import { breakpoints, iconButton } from '~components'
|
||||
import { DotFilledIcon } from '@radix-ui/react-icons'
|
||||
import { ToolsPanel } from '~components/ToolsPanel'
|
||||
import { TopPanel } from '~components/TopPanel'
|
||||
import { TLDR } from '~state/tldr'
|
||||
import { ContextMenu } from '~components/context-menu'
|
||||
import { ContextMenu } from '~components/ContextMenu'
|
||||
import { FocusButton } from '~components/FocusButton/FocusButton'
|
||||
|
||||
// Selectors
|
||||
const isInSelectSelector = (s: Data) => s.appState.activeTool === 'select'
|
||||
|
@ -68,6 +65,26 @@ export interface TLDrawProps {
|
|||
*/
|
||||
showPages?: boolean
|
||||
|
||||
/**
|
||||
* (optional) Whether to show the styles UI.
|
||||
*/
|
||||
showStyles?: boolean
|
||||
|
||||
/**
|
||||
* (optional) Whether to show the zoom UI.
|
||||
*/
|
||||
showZoom?: boolean
|
||||
|
||||
/**
|
||||
* (optional) Whether to show the tools UI.
|
||||
*/
|
||||
showTools?: boolean
|
||||
|
||||
/**
|
||||
* (optional) Whether to show the UI.
|
||||
*/
|
||||
showUI?: boolean
|
||||
|
||||
/**
|
||||
* (optional) A callback to run when the component mounts.
|
||||
*/
|
||||
|
@ -88,6 +105,10 @@ export function TLDraw({
|
|||
autofocus = true,
|
||||
showMenu = true,
|
||||
showPages = true,
|
||||
showTools = true,
|
||||
showZoom = true,
|
||||
showStyles = true,
|
||||
showUI = true,
|
||||
onMount,
|
||||
onChange,
|
||||
onUserChange,
|
||||
|
@ -120,27 +141,41 @@ export function TLDraw({
|
|||
autofocus={autofocus}
|
||||
showPages={showPages}
|
||||
showMenu={showMenu}
|
||||
showStyles={showStyles}
|
||||
showZoom={showZoom}
|
||||
showTools={showTools}
|
||||
showUI={showUI}
|
||||
/>
|
||||
</IdProvider>
|
||||
</TLDrawContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
interface InnerTLDrawProps {
|
||||
id?: string
|
||||
currentPageId?: string
|
||||
autofocus: boolean
|
||||
showPages: boolean
|
||||
showMenu: boolean
|
||||
showZoom: boolean
|
||||
showStyles: boolean
|
||||
showUI: boolean
|
||||
showTools: boolean
|
||||
document?: TLDrawDocument
|
||||
}
|
||||
|
||||
function InnerTldraw({
|
||||
id,
|
||||
currentPageId,
|
||||
autofocus,
|
||||
showPages,
|
||||
showMenu,
|
||||
showZoom,
|
||||
showStyles,
|
||||
showTools,
|
||||
showUI,
|
||||
document,
|
||||
}: {
|
||||
id?: string
|
||||
currentPageId?: string
|
||||
autofocus: boolean
|
||||
showPages: boolean
|
||||
showMenu: boolean
|
||||
document?: TLDrawDocument
|
||||
}) {
|
||||
}: InnerTLDrawProps) {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
|
||||
const rWrapper = React.useRef<HTMLDivElement>(null)
|
||||
|
@ -209,11 +244,7 @@ function InnerTldraw({
|
|||
}, [currentPageId, tlstate])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={rWrapper}
|
||||
tabIndex={0}
|
||||
className={[layout(), settings.isDarkMode ? dark : ''].join(' ')}
|
||||
>
|
||||
<StyledLayout ref={rWrapper} tabIndex={0} className={settings.isDarkMode ? dark : ''}>
|
||||
<OneOff focusableRef={rWrapper} autofocus={autofocus} />
|
||||
<ContextMenu>
|
||||
<Renderer
|
||||
|
@ -284,26 +315,25 @@ function InnerTldraw({
|
|||
onKeyUp={tlstate.onKeyUp}
|
||||
/>
|
||||
</ContextMenu>
|
||||
<div className={ui()}>
|
||||
{settings.isFocusMode ? (
|
||||
<div className={unfocusButton()}>
|
||||
<button className={iconButton({ bp: breakpoints })} onClick={tlstate.toggleFocusMode}>
|
||||
<DotFilledIcon />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={menuButtons()}>
|
||||
{showMenu && <Menu />}
|
||||
{showPages && <PagePanel />}
|
||||
</div>
|
||||
<div className={spacer()} />
|
||||
<StylePanel />
|
||||
<ToolsPanel />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showUI && (
|
||||
<StyledUI>
|
||||
{settings.isFocusMode ? (
|
||||
<FocusButton onSelect={tlstate.toggleFocusMode} />
|
||||
) : (
|
||||
<>
|
||||
<TopPanel
|
||||
showPages={showPages}
|
||||
showMenu={showMenu}
|
||||
showZoom={showZoom}
|
||||
showStyles={showStyles}
|
||||
/>
|
||||
<StyledSpacer />
|
||||
{showTools && <ToolsPanel />}
|
||||
</>
|
||||
)}
|
||||
</StyledUI>
|
||||
)}
|
||||
</StyledLayout>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -328,7 +358,7 @@ const OneOff = React.memo(
|
|||
}
|
||||
)
|
||||
|
||||
const layout = css({
|
||||
const StyledLayout = styled('div', {
|
||||
position: 'absolute',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
|
@ -350,7 +380,7 @@ const layout = css({
|
|||
},
|
||||
})
|
||||
|
||||
const ui = css({
|
||||
const StyledUI = styled('div', {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
|
@ -367,25 +397,6 @@ const ui = css({
|
|||
},
|
||||
})
|
||||
|
||||
const spacer = css({
|
||||
const StyledSpacer = styled('div', {
|
||||
flexGrow: 2,
|
||||
})
|
||||
|
||||
const menuButtons = css({
|
||||
display: 'flex',
|
||||
gap: 8,
|
||||
})
|
||||
|
||||
const unfocusButton = css({
|
||||
opacity: 1,
|
||||
zIndex: 100,
|
||||
backgroundColor: 'transparent',
|
||||
|
||||
'& svg': {
|
||||
color: '$muted',
|
||||
},
|
||||
|
||||
'&:hover svg': {
|
||||
color: '$text',
|
||||
},
|
||||
})
|
11
packages/tldraw/src/components/ContextMenu/CMIconButton.tsx
Normal file
11
packages/tldraw/src/components/ContextMenu/CMIconButton.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as React from 'react'
|
||||
import { ContextMenuItem } from '@radix-ui/react-context-menu'
|
||||
import { ToolButton, ToolButtonProps } from '~components/ToolButton'
|
||||
|
||||
export function CMIconButton({ onSelect, ...rest }: ToolButtonProps): JSX.Element {
|
||||
return (
|
||||
<ContextMenuItem dir="ltr" onSelect={onSelect} asChild>
|
||||
<ToolButton {...rest} />
|
||||
</ContextMenuItem>
|
||||
)
|
||||
}
|
11
packages/tldraw/src/components/ContextMenu/CMRowButton.tsx
Normal file
11
packages/tldraw/src/components/ContextMenu/CMRowButton.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as React from 'react'
|
||||
import { ContextMenuItem } from '@radix-ui/react-context-menu'
|
||||
import { RowButton, RowButtonProps } from '~components/RowButton'
|
||||
|
||||
export const CMRowButton = ({ onSelect, ...rest }: RowButtonProps) => {
|
||||
return (
|
||||
<ContextMenuItem asChild onSelect={onSelect}>
|
||||
<RowButton {...rest} />
|
||||
</ContextMenuItem>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import * as React from 'react'
|
||||
import { ContextMenuTriggerItem } from '@radix-ui/react-context-menu'
|
||||
import { RowButton, RowButtonProps } from '~components/RowButton'
|
||||
|
||||
interface CMTriggerButtonProps extends RowButtonProps {
|
||||
isSubmenu?: boolean
|
||||
}
|
||||
|
||||
export const CMTriggerButton = ({ isSubmenu, ...rest }: CMTriggerButtonProps) => {
|
||||
return (
|
||||
<ContextMenuTriggerItem asChild>
|
||||
<RowButton hasArrow={isSubmenu} {...rest} />
|
||||
</ContextMenuTriggerItem>
|
||||
)
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from 'react'
|
||||
import { ContextMenu } from './context-menu'
|
||||
import { ContextMenu } from './ContextMenu'
|
||||
import { renderWithContext } from '~test'
|
||||
|
||||
describe('context menu', () => {
|
372
packages/tldraw/src/components/ContextMenu/ContextMenu.tsx
Normal file
372
packages/tldraw/src/components/ContextMenu/ContextMenu.tsx
Normal file
|
@ -0,0 +1,372 @@
|
|||
import * as React from 'react'
|
||||
import styled from '~styles'
|
||||
import * as RadixContextMenu from '@radix-ui/react-context-menu'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { Data, AlignType, DistributeType, StretchType } from '~types'
|
||||
import {
|
||||
AlignBottomIcon,
|
||||
AlignCenterHorizontallyIcon,
|
||||
AlignCenterVerticallyIcon,
|
||||
AlignLeftIcon,
|
||||
AlignRightIcon,
|
||||
AlignTopIcon,
|
||||
SpaceEvenlyHorizontallyIcon,
|
||||
SpaceEvenlyVerticallyIcon,
|
||||
StretchHorizontallyIcon,
|
||||
StretchVerticallyIcon,
|
||||
} from '@radix-ui/react-icons'
|
||||
import { CMRowButton } from './CMRowButton'
|
||||
import { CMIconButton } from './CMIconButton'
|
||||
import { CMTriggerButton } from './CMTriggerButton'
|
||||
import { Divider } from '~components/Divider'
|
||||
import { MenuContent } from '~components/MenuContent'
|
||||
|
||||
const has1SelectedIdsSelector = (s: Data) => {
|
||||
return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 0
|
||||
}
|
||||
const has2SelectedIdsSelector = (s: Data) => {
|
||||
return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 1
|
||||
}
|
||||
const has3SelectedIdsSelector = (s: Data) => {
|
||||
return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 2
|
||||
}
|
||||
|
||||
const isDebugModeSelector = (s: Data) => {
|
||||
return s.settings.isDebugMode
|
||||
}
|
||||
|
||||
const hasGroupSelectedSelector = (s: Data) => {
|
||||
return s.document.pageStates[s.appState.currentPageId].selectedIds.some(
|
||||
(id) => s.document.pages[s.appState.currentPageId].shapes[id].children !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
interface ContextMenuProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const ContextMenu = ({ children }: ContextMenuProps): JSX.Element => {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const hasSelection = useSelector(has1SelectedIdsSelector)
|
||||
const hasTwoOrMore = useSelector(has2SelectedIdsSelector)
|
||||
const hasThreeOrMore = useSelector(has3SelectedIdsSelector)
|
||||
const isDebugMode = useSelector(isDebugModeSelector)
|
||||
const hasGroupSelected = useSelector(hasGroupSelectedSelector)
|
||||
|
||||
const rContent = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleFlipHorizontal = React.useCallback(() => {
|
||||
tlstate.flipHorizontal()
|
||||
}, [tlstate])
|
||||
|
||||
const handleFlipVertical = React.useCallback(() => {
|
||||
tlstate.flipVertical()
|
||||
}, [tlstate])
|
||||
|
||||
const handleDuplicate = React.useCallback(() => {
|
||||
tlstate.duplicate()
|
||||
}, [tlstate])
|
||||
|
||||
const handleGroup = React.useCallback(() => {
|
||||
tlstate.group()
|
||||
}, [tlstate])
|
||||
|
||||
const handleMoveToBack = React.useCallback(() => {
|
||||
tlstate.moveToBack()
|
||||
}, [tlstate])
|
||||
|
||||
const handleMoveBackward = React.useCallback(() => {
|
||||
tlstate.moveBackward()
|
||||
}, [tlstate])
|
||||
|
||||
const handleMoveForward = React.useCallback(() => {
|
||||
tlstate.moveForward()
|
||||
}, [tlstate])
|
||||
|
||||
const handleMoveToFront = React.useCallback(() => {
|
||||
tlstate.moveToFront()
|
||||
}, [tlstate])
|
||||
|
||||
const handleDelete = React.useCallback(() => {
|
||||
tlstate.delete()
|
||||
}, [tlstate])
|
||||
|
||||
const handleCopyJson = React.useCallback(() => {
|
||||
tlstate.copyJson()
|
||||
}, [tlstate])
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
tlstate.copy()
|
||||
}, [tlstate])
|
||||
|
||||
const handlePaste = React.useCallback(() => {
|
||||
tlstate.paste()
|
||||
}, [tlstate])
|
||||
|
||||
const handleCopySvg = React.useCallback(() => {
|
||||
tlstate.copySvg()
|
||||
}, [tlstate])
|
||||
|
||||
const handleUndo = React.useCallback(() => {
|
||||
tlstate.undo()
|
||||
}, [tlstate])
|
||||
|
||||
const handleRedo = React.useCallback(() => {
|
||||
tlstate.redo()
|
||||
}, [tlstate])
|
||||
|
||||
return (
|
||||
<RadixContextMenu.Root>
|
||||
<RadixContextMenu.Trigger dir="ltr">{children}</RadixContextMenu.Trigger>
|
||||
<RadixContextMenu.Content dir="ltr" ref={rContent} asChild>
|
||||
<MenuContent>
|
||||
{hasSelection ? (
|
||||
<>
|
||||
<CMRowButton onSelect={handleFlipHorizontal} kbd="⇧H">
|
||||
Flip Horizontal
|
||||
</CMRowButton>
|
||||
<CMRowButton onSelect={handleFlipVertical} kbd="⇧V">
|
||||
Flip Vertical
|
||||
</CMRowButton>
|
||||
<CMRowButton onSelect={handleDuplicate} kbd="#D">
|
||||
Duplicate
|
||||
</CMRowButton>
|
||||
<Divider />
|
||||
{hasTwoOrMore && (
|
||||
<CMRowButton onSelect={handleGroup} kbd="#G">
|
||||
Group
|
||||
</CMRowButton>
|
||||
)}
|
||||
<Divider />
|
||||
{hasGroupSelected && (
|
||||
<CMRowButton onSelect={handleGroup} kbd="#⇧G">
|
||||
Ungroup
|
||||
</CMRowButton>
|
||||
)}
|
||||
<ContextMenuSubMenu label="Move">
|
||||
<CMRowButton onSelect={handleMoveToFront} kbd="⇧]">
|
||||
To Front
|
||||
</CMRowButton>
|
||||
<CMRowButton onSelect={handleMoveForward} kbd="]">
|
||||
Forward
|
||||
</CMRowButton>
|
||||
<CMRowButton onSelect={handleMoveBackward} kbd="[">
|
||||
Backward
|
||||
</CMRowButton>
|
||||
<CMRowButton onSelect={handleMoveToBack} kbd="⇧[">
|
||||
To Back
|
||||
</CMRowButton>
|
||||
</ContextMenuSubMenu>
|
||||
<MoveToPageMenu />
|
||||
{hasTwoOrMore && (
|
||||
<AlignDistributeSubMenu
|
||||
hasTwoOrMore={hasTwoOrMore}
|
||||
hasThreeOrMore={hasThreeOrMore}
|
||||
/>
|
||||
)}
|
||||
<Divider />
|
||||
<CMRowButton onSelect={handleCopy} kbd="#C">
|
||||
Copy
|
||||
</CMRowButton>
|
||||
<CMRowButton onSelect={handleCopySvg} kbd="⇧#C">
|
||||
Copy as SVG
|
||||
</CMRowButton>
|
||||
{isDebugMode && <CMRowButton onSelect={handleCopyJson}>Copy as JSON</CMRowButton>}
|
||||
<CMRowButton onSelect={handlePaste} kbd="#V">
|
||||
Paste
|
||||
</CMRowButton>
|
||||
<Divider />
|
||||
<CMRowButton onSelect={handleDelete} kbd="⌫">
|
||||
Delete
|
||||
</CMRowButton>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<CMRowButton onSelect={handlePaste} kbd="#V">
|
||||
Paste
|
||||
</CMRowButton>
|
||||
<CMRowButton onSelect={handleUndo} kbd="#Z">
|
||||
Undo
|
||||
</CMRowButton>
|
||||
<CMRowButton onSelect={handleRedo} kbd="#⇧Z">
|
||||
Redo
|
||||
</CMRowButton>
|
||||
</>
|
||||
)}
|
||||
</MenuContent>
|
||||
</RadixContextMenu.Content>
|
||||
</RadixContextMenu.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function AlignDistributeSubMenu({
|
||||
hasThreeOrMore,
|
||||
}: {
|
||||
hasTwoOrMore: boolean
|
||||
hasThreeOrMore: boolean
|
||||
}) {
|
||||
const { tlstate } = useTLDrawContext()
|
||||
|
||||
const alignTop = React.useCallback(() => {
|
||||
tlstate.align(AlignType.Top)
|
||||
}, [tlstate])
|
||||
|
||||
const alignCenterVertical = React.useCallback(() => {
|
||||
tlstate.align(AlignType.CenterVertical)
|
||||
}, [tlstate])
|
||||
|
||||
const alignBottom = React.useCallback(() => {
|
||||
tlstate.align(AlignType.Bottom)
|
||||
}, [tlstate])
|
||||
|
||||
const stretchVertically = React.useCallback(() => {
|
||||
tlstate.stretch(StretchType.Vertical)
|
||||
}, [tlstate])
|
||||
|
||||
const distributeVertically = React.useCallback(() => {
|
||||
tlstate.distribute(DistributeType.Vertical)
|
||||
}, [tlstate])
|
||||
|
||||
const alignLeft = React.useCallback(() => {
|
||||
tlstate.align(AlignType.Left)
|
||||
}, [tlstate])
|
||||
|
||||
const alignCenterHorizontal = React.useCallback(() => {
|
||||
tlstate.align(AlignType.CenterHorizontal)
|
||||
}, [tlstate])
|
||||
|
||||
const alignRight = React.useCallback(() => {
|
||||
tlstate.align(AlignType.Right)
|
||||
}, [tlstate])
|
||||
|
||||
const stretchHorizontally = React.useCallback(() => {
|
||||
tlstate.stretch(StretchType.Horizontal)
|
||||
}, [tlstate])
|
||||
|
||||
const distributeHorizontally = React.useCallback(() => {
|
||||
tlstate.distribute(DistributeType.Horizontal)
|
||||
}, [tlstate])
|
||||
|
||||
return (
|
||||
<RadixContextMenu.Root>
|
||||
<CMTriggerButton isSubmenu>Align / Distribute</CMTriggerButton>
|
||||
<RadixContextMenu.Content asChild sideOffset={2} alignOffset={-2}>
|
||||
<StyledGridContent selectedStyle={hasThreeOrMore ? 'threeOrMore' : 'twoOrMore'}>
|
||||
<CMIconButton onSelect={alignLeft}>
|
||||
<AlignLeftIcon />
|
||||
</CMIconButton>
|
||||
<CMIconButton onSelect={alignCenterHorizontal}>
|
||||
<AlignCenterHorizontallyIcon />
|
||||
</CMIconButton>
|
||||
<CMIconButton onSelect={alignRight}>
|
||||
<AlignRightIcon />
|
||||
</CMIconButton>
|
||||
<CMIconButton onSelect={stretchHorizontally}>
|
||||
<StretchHorizontallyIcon />
|
||||
</CMIconButton>
|
||||
{hasThreeOrMore && (
|
||||
<CMIconButton onSelect={distributeHorizontally}>
|
||||
<SpaceEvenlyHorizontallyIcon />
|
||||
</CMIconButton>
|
||||
)}
|
||||
<CMIconButton onSelect={alignTop}>
|
||||
<AlignTopIcon />
|
||||
</CMIconButton>
|
||||
<CMIconButton onSelect={alignCenterVertical}>
|
||||
<AlignCenterVerticallyIcon />
|
||||
</CMIconButton>
|
||||
<CMIconButton onSelect={alignBottom}>
|
||||
<AlignBottomIcon />
|
||||
</CMIconButton>
|
||||
<CMIconButton onSelect={stretchVertically}>
|
||||
<StretchVerticallyIcon />
|
||||
</CMIconButton>
|
||||
{hasThreeOrMore && (
|
||||
<CMIconButton onSelect={distributeVertically}>
|
||||
<SpaceEvenlyVerticallyIcon />
|
||||
</CMIconButton>
|
||||
)}
|
||||
<CMArrow offset={13} />
|
||||
</StyledGridContent>
|
||||
</RadixContextMenu.Content>
|
||||
</RadixContextMenu.Root>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledGridContent = styled(MenuContent, {
|
||||
display: 'grid',
|
||||
variants: {
|
||||
selectedStyle: {
|
||||
threeOrMore: {
|
||||
gridTemplateColumns: 'repeat(5, auto)',
|
||||
},
|
||||
twoOrMore: {
|
||||
gridTemplateColumns: 'repeat(4, auto)',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
/* ------------------ Move to Page ------------------ */
|
||||
|
||||
const currentPageIdSelector = (s: Data) => s.appState.currentPageId
|
||||
const documentPagesSelector = (s: Data) => s.document.pages
|
||||
|
||||
function MoveToPageMenu(): JSX.Element | null {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const currentPageId = useSelector(currentPageIdSelector)
|
||||
const documentPages = useSelector(documentPagesSelector)
|
||||
|
||||
const sorted = Object.values(documentPages)
|
||||
.sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0))
|
||||
.filter((a) => a.id !== currentPageId)
|
||||
|
||||
if (sorted.length === 0) return null
|
||||
|
||||
return (
|
||||
<RadixContextMenu.Root dir="ltr">
|
||||
<CMTriggerButton isSubmenu>Move To Page</CMTriggerButton>
|
||||
<RadixContextMenu.Content dir="ltr" sideOffset={2} alignOffset={-2} asChild>
|
||||
<MenuContent>
|
||||
{sorted.map(({ id, name }, i) => (
|
||||
<CMRowButton
|
||||
key={id}
|
||||
disabled={id === currentPageId}
|
||||
onSelect={() => tlstate.moveToPage(id)}
|
||||
>
|
||||
{name || `Page ${i}`}
|
||||
</CMRowButton>
|
||||
))}
|
||||
<CMArrow offset={13} />
|
||||
</MenuContent>
|
||||
</RadixContextMenu.Content>
|
||||
</RadixContextMenu.Root>
|
||||
)
|
||||
}
|
||||
|
||||
/* --------------------- Submenu -------------------- */
|
||||
|
||||
export interface ContextMenuSubMenuProps {
|
||||
label: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function ContextMenuSubMenu({ children, label }: ContextMenuSubMenuProps): JSX.Element {
|
||||
return (
|
||||
<RadixContextMenu.Root dir="ltr">
|
||||
<CMTriggerButton isSubmenu>{label}</CMTriggerButton>
|
||||
<RadixContextMenu.Content dir="ltr" sideOffset={2} alignOffset={-2} asChild>
|
||||
<MenuContent>
|
||||
{children}
|
||||
<CMArrow offset={13} />
|
||||
</MenuContent>
|
||||
</RadixContextMenu.Content>
|
||||
</RadixContextMenu.Root>
|
||||
)
|
||||
}
|
||||
|
||||
/* ---------------------- Arrow --------------------- */
|
||||
|
||||
const CMArrow = styled(RadixContextMenu.ContextMenuArrow, {
|
||||
fill: '$panel',
|
||||
})
|
1
packages/tldraw/src/components/ContextMenu/index.ts
Normal file
1
packages/tldraw/src/components/ContextMenu/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './ContextMenu'
|
12
packages/tldraw/src/components/Divider/Divider.tsx
Normal file
12
packages/tldraw/src/components/Divider/Divider.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import * as React from 'react'
|
||||
import styled from '~styles'
|
||||
|
||||
export const Divider = styled('hr', {
|
||||
height: 1,
|
||||
marginTop: '$1',
|
||||
marginRight: '-$2',
|
||||
marginBottom: '$1',
|
||||
marginLeft: '-$2',
|
||||
border: 'none',
|
||||
borderBottom: '1px solid $hover',
|
||||
})
|
1
packages/tldraw/src/components/Divider/index.ts
Normal file
1
packages/tldraw/src/components/Divider/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Divider'
|
5
packages/tldraw/src/components/DropdownMenu/DMArrow.tsx
Normal file
5
packages/tldraw/src/components/DropdownMenu/DMArrow.tsx
Normal file
|
@ -0,0 +1,5 @@
|
|||
import { Arrow } from '@radix-ui/react-dropdown-menu'
|
||||
import { breakpoints } from '~components/breakpoints'
|
||||
import styled from '~styles/stitches.config'
|
||||
|
||||
export const DMArrow = styled(Arrow, { fill: '$panel', bp: breakpoints })
|
|
@ -0,0 +1,33 @@
|
|||
import * as React from 'react'
|
||||
import { CheckboxItem } from '@radix-ui/react-dropdown-menu'
|
||||
import { RowButton } from '~components/RowButton'
|
||||
|
||||
interface DMCheckboxItemProps {
|
||||
checked: boolean
|
||||
disabled?: boolean
|
||||
onCheckedChange: (isChecked: boolean) => void
|
||||
children: React.ReactNode
|
||||
kbd?: string
|
||||
}
|
||||
|
||||
export function DMCheckboxItem({
|
||||
checked,
|
||||
disabled = false,
|
||||
onCheckedChange,
|
||||
kbd,
|
||||
children,
|
||||
}: DMCheckboxItemProps): JSX.Element {
|
||||
return (
|
||||
<CheckboxItem
|
||||
dir="ltr"
|
||||
onCheckedChange={onCheckedChange}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
asChild
|
||||
>
|
||||
<RowButton kbd={kbd} hasIndicator>
|
||||
{children}
|
||||
</RowButton>
|
||||
</CheckboxItem>
|
||||
)
|
||||
}
|
36
packages/tldraw/src/components/DropdownMenu/DMContent.tsx
Normal file
36
packages/tldraw/src/components/DropdownMenu/DMContent.tsx
Normal file
|
@ -0,0 +1,36 @@
|
|||
import * as React from 'react'
|
||||
import { Content } from '@radix-ui/react-dropdown-menu'
|
||||
import styled from '~styles/stitches.config'
|
||||
import { MenuContent } from '~components/MenuContent'
|
||||
|
||||
export interface DMContentProps {
|
||||
variant?: 'grid' | 'menu'
|
||||
align?: 'start' | 'center' | 'end'
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function DMContent({ children, align, variant }: DMContentProps): JSX.Element {
|
||||
return (
|
||||
<Content sideOffset={8} dir="ltr" asChild align={align}>
|
||||
<StyledContent variant={variant}>{children}</StyledContent>
|
||||
</Content>
|
||||
)
|
||||
}
|
||||
|
||||
export const StyledContent = styled(MenuContent, {
|
||||
width: 'fit-content',
|
||||
height: 'fit-content',
|
||||
minWidth: 0,
|
||||
variants: {
|
||||
variant: {
|
||||
grid: {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(4, auto)',
|
||||
gap: 0,
|
||||
},
|
||||
menu: {
|
||||
minWidth: 128,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
11
packages/tldraw/src/components/DropdownMenu/DMDivider.tsx
Normal file
11
packages/tldraw/src/components/DropdownMenu/DMDivider.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Separator } from '@radix-ui/react-dropdown-menu'
|
||||
import styled from '~styles/stitches.config'
|
||||
|
||||
export const DMDivider = styled(Separator, {
|
||||
backgroundColor: '$hover',
|
||||
height: 1,
|
||||
marginTop: '$2',
|
||||
marginRight: '-$2',
|
||||
marginBottom: '$2',
|
||||
marginLeft: '-$2',
|
||||
})
|
23
packages/tldraw/src/components/DropdownMenu/DMIconButton.tsx
Normal file
23
packages/tldraw/src/components/DropdownMenu/DMIconButton.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import * as React from 'react'
|
||||
import { Item } from '@radix-ui/react-dropdown-menu'
|
||||
import { IconButton } from '~components/IconButton/IconButton'
|
||||
|
||||
interface DMIconButtonProps {
|
||||
onSelect: () => void
|
||||
disabled?: boolean
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function DMIconButton({
|
||||
onSelect,
|
||||
children,
|
||||
disabled = false,
|
||||
}: DMIconButtonProps): JSX.Element {
|
||||
return (
|
||||
<Item dir="ltr" asChild>
|
||||
<IconButton disabled={disabled} onSelect={onSelect}>
|
||||
{children}
|
||||
</IconButton>
|
||||
</Item>
|
||||
)
|
||||
}
|
11
packages/tldraw/src/components/DropdownMenu/DMItem.tsx
Normal file
11
packages/tldraw/src/components/DropdownMenu/DMItem.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import * as React from 'react'
|
||||
import { Item } from '@radix-ui/react-dropdown-menu'
|
||||
import { RowButton, RowButtonProps } from '~components/RowButton'
|
||||
|
||||
export function DMItem({ onSelect, ...rest }: RowButtonProps): JSX.Element {
|
||||
return (
|
||||
<Item dir="ltr" asChild onSelect={onSelect}>
|
||||
<RowButton {...rest} />
|
||||
</Item>
|
||||
)
|
||||
}
|
26
packages/tldraw/src/components/DropdownMenu/DMRadioItem.tsx
Normal file
26
packages/tldraw/src/components/DropdownMenu/DMRadioItem.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { RadioItem } from '@radix-ui/react-dropdown-menu'
|
||||
import styled from '~styles/stitches.config'
|
||||
|
||||
export const DMRadioItem = styled(RadioItem, {
|
||||
height: '32px',
|
||||
width: '32px',
|
||||
backgroundColor: '$panel',
|
||||
borderRadius: '4px',
|
||||
padding: '0',
|
||||
margin: '0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
outline: 'none',
|
||||
border: 'none',
|
||||
pointerEvents: 'all',
|
||||
cursor: 'pointer',
|
||||
|
||||
'&:focus': {
|
||||
backgroundColor: '$hover',
|
||||
},
|
||||
|
||||
'&:hover:not(:disabled)': {
|
||||
backgroundColor: '$hover',
|
||||
},
|
||||
})
|
28
packages/tldraw/src/components/DropdownMenu/DMSubMenu.tsx
Normal file
28
packages/tldraw/src/components/DropdownMenu/DMSubMenu.tsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import * as React from 'react'
|
||||
import { Root, TriggerItem, Content, Arrow } from '@radix-ui/react-dropdown-menu'
|
||||
import { RowButton } from '~components/RowButton'
|
||||
import { MenuContent } from '~components/MenuContent'
|
||||
|
||||
export interface DMSubMenuProps {
|
||||
label: string
|
||||
disabled?: boolean
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function DMSubMenu({ children, disabled = false, label }: DMSubMenuProps): JSX.Element {
|
||||
return (
|
||||
<Root dir="ltr">
|
||||
<TriggerItem dir="ltr" asChild>
|
||||
<RowButton disabled={disabled} hasArrow>
|
||||
{label}
|
||||
</RowButton>
|
||||
</TriggerItem>
|
||||
<Content dir="ltr" asChild sideOffset={2} alignOffset={-2}>
|
||||
<MenuContent>
|
||||
{children}
|
||||
<Arrow offset={13} />
|
||||
</MenuContent>
|
||||
</Content>
|
||||
</Root>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import * as React from 'react'
|
||||
import { Trigger } from '@radix-ui/react-dropdown-menu'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
|
||||
interface DMTriggerIconProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function DMTriggerIcon({ children }: DMTriggerIconProps) {
|
||||
return (
|
||||
<Trigger asChild>
|
||||
<ToolButton>{children}</ToolButton>
|
||||
</Trigger>
|
||||
)
|
||||
}
|
9
packages/tldraw/src/components/DropdownMenu/index.tsx
Normal file
9
packages/tldraw/src/components/DropdownMenu/index.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
export * from './DMArrow'
|
||||
export * from './DMItem'
|
||||
export * from './DMCheckboxItem'
|
||||
export * from './DMContent'
|
||||
export * from './DMDivider'
|
||||
export * from './DMIconButton'
|
||||
export * from './DMRadioItem'
|
||||
export * from './DMSubMenu'
|
||||
export * from './DMTriggerIcon'
|
32
packages/tldraw/src/components/FocusButton/FocusButton.tsx
Normal file
32
packages/tldraw/src/components/FocusButton/FocusButton.tsx
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { DotFilledIcon } from '@radix-ui/react-icons'
|
||||
import * as React from 'react'
|
||||
import { IconButton } from '~components/IconButton/IconButton'
|
||||
import styled from '~styles'
|
||||
|
||||
interface FocusButtonProps {
|
||||
onSelect: () => void
|
||||
}
|
||||
|
||||
export function FocusButton({ onSelect }: FocusButtonProps) {
|
||||
return (
|
||||
<StyledButtonContainer>
|
||||
<IconButton onClick={onSelect}>
|
||||
<DotFilledIcon />
|
||||
</IconButton>
|
||||
</StyledButtonContainer>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledButtonContainer = styled('div', {
|
||||
opacity: 1,
|
||||
zIndex: 100,
|
||||
backgroundColor: 'transparent',
|
||||
|
||||
'& svg': {
|
||||
color: '$muted',
|
||||
},
|
||||
|
||||
'&:hover svg': {
|
||||
color: '$text',
|
||||
},
|
||||
})
|
0
packages/tldraw/src/components/FocusButton/index.ts
Normal file
0
packages/tldraw/src/components/FocusButton/index.ts
Normal file
|
@ -1,10 +1,6 @@
|
|||
import css from '~styles'
|
||||
import styled from '~styles'
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Icon Button */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
export const iconButton = css({
|
||||
export const IconButton = styled('button', {
|
||||
position: 'relative',
|
||||
height: '32px',
|
||||
width: '32px',
|
||||
|
@ -12,15 +8,15 @@ export const iconButton = css({
|
|||
borderRadius: '4px',
|
||||
padding: '0',
|
||||
margin: '0',
|
||||
display: 'grid',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
outline: 'none',
|
||||
border: 'none',
|
||||
pointerEvents: 'all',
|
||||
fontSize: '$0',
|
||||
color: '$text',
|
||||
cursor: 'pointer',
|
||||
display: 'grid',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
|
||||
'& > *': {
|
||||
gridRow: 1,
|
1
packages/tldraw/src/components/IconButton/index.ts
Normal file
1
packages/tldraw/src/components/IconButton/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './IconButton'
|
|
@ -1,14 +1,12 @@
|
|||
import * as React from 'react'
|
||||
import css from '~styles'
|
||||
import styled from '~styles'
|
||||
import { Utils } from '@tldraw/core'
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Keyboard Shortcut */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
export function commandKey(): string {
|
||||
return Utils.isDarwin() ? '⌘' : 'Ctrl'
|
||||
}
|
||||
const commandKey = () => (Utils.isDarwin() ? '⌘' : 'Ctrl')
|
||||
|
||||
export function Kbd({
|
||||
variant,
|
||||
|
@ -18,18 +16,18 @@ export function Kbd({
|
|||
children: string
|
||||
}): JSX.Element | null {
|
||||
return (
|
||||
<kbd className={kbd({ variant })}>
|
||||
<StyledKbd variant={variant}>
|
||||
{children
|
||||
.replaceAll('#', commandKey())
|
||||
.split('')
|
||||
.map((k, i) => (
|
||||
<span key={i}>{k}</span>
|
||||
))}
|
||||
</kbd>
|
||||
</StyledKbd>
|
||||
)
|
||||
}
|
||||
|
||||
export const kbd = css({
|
||||
export const StyledKbd = styled('kbd', {
|
||||
marginLeft: '$3',
|
||||
textShadow: '$2',
|
||||
textAlign: 'center',
|
1
packages/tldraw/src/components/Kbd/index.ts
Normal file
1
packages/tldraw/src/components/Kbd/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Kbd'
|
17
packages/tldraw/src/components/MenuContent/MenuContent.ts
Normal file
17
packages/tldraw/src/components/MenuContent/MenuContent.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import styled from '~styles'
|
||||
|
||||
export const MenuContent = styled('div', {
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
userSelect: 'none',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
zIndex: 180,
|
||||
minWidth: 180,
|
||||
pointerEvents: 'all',
|
||||
backgroundColor: '$panel',
|
||||
boxShadow: '$panel',
|
||||
padding: '$2 $2',
|
||||
borderRadius: '$3',
|
||||
font: '$ui',
|
||||
})
|
1
packages/tldraw/src/components/MenuContent/index.ts
Normal file
1
packages/tldraw/src/components/MenuContent/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './MenuContent'
|
30
packages/tldraw/src/components/Panel/Panel.tsx
Normal file
30
packages/tldraw/src/components/Panel/Panel.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import styled from '~styles/stitches.config'
|
||||
|
||||
export const Panel = styled('div', {
|
||||
backgroundColor: '$panel',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
padding: '0 $2',
|
||||
boxShadow: '$panel',
|
||||
variants: {
|
||||
side: {
|
||||
center: {
|
||||
borderTopLeftRadius: '$4',
|
||||
borderTopRightRadius: '$4',
|
||||
// borderTop: '1px solid $panelBorder',
|
||||
// borderLeft: '1px solid $panelBorder',
|
||||
// borderRight: '1px solid $panelBorder',
|
||||
},
|
||||
left: {
|
||||
borderBottomRightRadius: '$4',
|
||||
// borderBottom: '1px solid $panelBorder',
|
||||
// borderRight: '1px solid $panelBorder',
|
||||
},
|
||||
right: {
|
||||
borderBottomLeftRadius: '$4',
|
||||
// borderBottom: '1px solid $panelBorder',
|
||||
// borderLeft: '1px solid $panelBorder',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
1
packages/tldraw/src/components/Panel/index.ts
Normal file
1
packages/tldraw/src/components/Panel/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Panel'
|
142
packages/tldraw/src/components/RowButton/RowButton.tsx
Normal file
142
packages/tldraw/src/components/RowButton/RowButton.tsx
Normal file
|
@ -0,0 +1,142 @@
|
|||
import { ItemIndicator } from '@radix-ui/react-dropdown-menu'
|
||||
import { ChevronRightIcon, CheckIcon } from '@radix-ui/react-icons'
|
||||
import * as React from 'react'
|
||||
import { breakpoints } from '~components/breakpoints'
|
||||
import { Kbd } from '~components/Kbd'
|
||||
import { SmallIcon } from '~components/SmallIcon'
|
||||
import styled from '~styles'
|
||||
|
||||
export interface RowButtonProps {
|
||||
onSelect?: () => void
|
||||
children: React.ReactNode
|
||||
disabled?: boolean
|
||||
kbd?: string
|
||||
isActive?: boolean
|
||||
isWarning?: boolean
|
||||
hasIndicator?: boolean
|
||||
hasArrow?: boolean
|
||||
}
|
||||
|
||||
export const RowButton = React.forwardRef<HTMLButtonElement, RowButtonProps>(
|
||||
(
|
||||
{
|
||||
onSelect,
|
||||
isActive = false,
|
||||
isWarning = false,
|
||||
hasIndicator = false,
|
||||
hasArrow = false,
|
||||
disabled = false,
|
||||
kbd,
|
||||
children,
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
return (
|
||||
<StyledRowButton
|
||||
ref={ref}
|
||||
bp={breakpoints}
|
||||
isWarning={isWarning}
|
||||
isActive={isActive}
|
||||
disabled={disabled}
|
||||
onPointerDown={onSelect}
|
||||
{...rest}
|
||||
>
|
||||
<StyledRowButtonInner>
|
||||
{children}
|
||||
{kbd ? <Kbd variant="menu">{kbd}</Kbd> : undefined}
|
||||
{hasIndicator && (
|
||||
<ItemIndicator dir="ltr">
|
||||
<SmallIcon>
|
||||
<CheckIcon />
|
||||
</SmallIcon>
|
||||
</ItemIndicator>
|
||||
)}
|
||||
{hasArrow && (
|
||||
<SmallIcon>
|
||||
<ChevronRightIcon />
|
||||
</SmallIcon>
|
||||
)}
|
||||
</StyledRowButtonInner>
|
||||
</StyledRowButton>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
const StyledRowButtonInner = styled('div', {
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
color: '$text',
|
||||
fontFamily: '$ui',
|
||||
fontWeight: 400,
|
||||
fontSize: '$1',
|
||||
backgroundColor: '$panel',
|
||||
borderRadius: '$2',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: '0 $3',
|
||||
justifyContent: 'space-between',
|
||||
border: '1px solid transparent',
|
||||
|
||||
'& svg': {
|
||||
position: 'relative',
|
||||
stroke: '$overlay',
|
||||
strokeWidth: 1,
|
||||
zIndex: 1,
|
||||
},
|
||||
})
|
||||
|
||||
export const StyledRowButton = styled('button', {
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
height: '32px',
|
||||
outline: 'none',
|
||||
borderRadius: 4,
|
||||
userSelect: 'none',
|
||||
margin: 0,
|
||||
padding: '0 0',
|
||||
|
||||
'&[data-disabled]': {
|
||||
opacity: 0.3,
|
||||
},
|
||||
|
||||
'&:disabled': {
|
||||
opacity: 0.3,
|
||||
},
|
||||
|
||||
variants: {
|
||||
bp: {
|
||||
mobile: {},
|
||||
small: {},
|
||||
},
|
||||
size: {
|
||||
icon: {
|
||||
padding: '4px ',
|
||||
width: 'auto',
|
||||
},
|
||||
},
|
||||
isWarning: {
|
||||
true: {
|
||||
color: '$warn',
|
||||
},
|
||||
},
|
||||
isActive: {
|
||||
true: {
|
||||
backgroundColor: '$hover',
|
||||
},
|
||||
false: {
|
||||
[`&:hover:not(:disabled) ${StyledRowButtonInner}`]: {
|
||||
backgroundColor: '$hover',
|
||||
border: '1px solid $panel',
|
||||
'& *[data-shy="true"]': {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
1
packages/tldraw/src/components/RowButton/index.ts
Normal file
1
packages/tldraw/src/components/RowButton/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './RowButton'
|
27
packages/tldraw/src/components/SmallIcon/SmallIcon.tsx
Normal file
27
packages/tldraw/src/components/SmallIcon/SmallIcon.tsx
Normal file
|
@ -0,0 +1,27 @@
|
|||
import styled from '~styles'
|
||||
|
||||
export const SmallIcon = styled('div', {
|
||||
height: '100%',
|
||||
borderRadius: '4px',
|
||||
marginRight: '1px',
|
||||
width: 'fit-content',
|
||||
display: 'grid',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
outline: 'none',
|
||||
border: 'none',
|
||||
pointerEvents: 'all',
|
||||
cursor: 'pointer',
|
||||
color: '$text',
|
||||
|
||||
'& svg': {
|
||||
height: 16,
|
||||
width: 16,
|
||||
strokeWidth: 1,
|
||||
},
|
||||
|
||||
'& > *': {
|
||||
gridRow: 1,
|
||||
gridColumn: 1,
|
||||
},
|
||||
})
|
1
packages/tldraw/src/components/SmallIcon/index.ts
Normal file
1
packages/tldraw/src/components/SmallIcon/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './SmallIcon'
|
132
packages/tldraw/src/components/ToolButton/ToolButton.tsx
Normal file
132
packages/tldraw/src/components/ToolButton/ToolButton.tsx
Normal file
|
@ -0,0 +1,132 @@
|
|||
import * as React from 'react'
|
||||
import { Tooltip } from '~components/Tooltip'
|
||||
import styled from '~styles'
|
||||
|
||||
export interface ToolButtonProps {
|
||||
onSelect?: () => void
|
||||
onDoubleClick?: () => void
|
||||
isActive?: boolean
|
||||
variant?: 'icon' | 'text' | 'circle' | 'primary'
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const ToolButton = React.forwardRef<HTMLButtonElement, ToolButtonProps>(
|
||||
({ onSelect, onDoubleClick, isActive = false, variant, children, ...rest }, ref) => {
|
||||
return (
|
||||
<StyledToolButton
|
||||
ref={ref}
|
||||
isActive={isActive}
|
||||
variant={variant}
|
||||
onPointerDown={onSelect}
|
||||
onDoubleClick={onDoubleClick}
|
||||
{...rest}
|
||||
>
|
||||
<StyledToolButtonInner isActive={isActive} variant={variant}>
|
||||
{children}
|
||||
</StyledToolButtonInner>
|
||||
</StyledToolButton>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
/* ------------------ With Tooltip ------------------ */
|
||||
|
||||
interface ToolButtonWithTooltipProps extends ToolButtonProps {
|
||||
label: string
|
||||
kbd?: string
|
||||
}
|
||||
|
||||
export function ToolButtonWithTooltip({ label, kbd, ...rest }: ToolButtonWithTooltipProps) {
|
||||
return (
|
||||
<Tooltip label={label[0].toUpperCase() + label.slice(1)} kbd={kbd}>
|
||||
<ToolButton variant="primary" {...rest} />
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export const StyledToolButtonInner = styled('div', {
|
||||
position: 'relative',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
color: '$text',
|
||||
backgroundColor: '$panel',
|
||||
borderRadius: '$2',
|
||||
margin: '0',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontFamily: '$ui',
|
||||
userSelect: 'none',
|
||||
boxSizing: 'border-box',
|
||||
border: '1px solid transparent',
|
||||
|
||||
variants: {
|
||||
variant: {
|
||||
primary: {
|
||||
'& svg': {
|
||||
width: 20,
|
||||
height: 20,
|
||||
},
|
||||
},
|
||||
icon: {
|
||||
display: 'grid',
|
||||
'& > *': {
|
||||
gridRow: 1,
|
||||
gridColumn: 1,
|
||||
},
|
||||
},
|
||||
text: {
|
||||
fontSize: '$1',
|
||||
padding: '0 $3',
|
||||
},
|
||||
circle: {
|
||||
borderRadius: '100%',
|
||||
boxShadow: '$panel',
|
||||
},
|
||||
},
|
||||
isActive: {
|
||||
true: {
|
||||
backgroundColor: '$selected',
|
||||
color: '$panelActive',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const StyledToolButton = styled('button', {
|
||||
position: 'relative',
|
||||
color: '$text',
|
||||
height: '48px',
|
||||
width: '40px',
|
||||
fontSize: '$0',
|
||||
background: 'none',
|
||||
margin: '0',
|
||||
padding: '$3 $2',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
outline: 'none',
|
||||
cursor: 'pointer',
|
||||
pointerEvents: 'all',
|
||||
border: 'none',
|
||||
|
||||
variants: {
|
||||
variant: {
|
||||
primary: {},
|
||||
icon: {},
|
||||
text: {
|
||||
width: 'auto',
|
||||
},
|
||||
circle: {},
|
||||
},
|
||||
isActive: {
|
||||
true: {},
|
||||
false: {
|
||||
[`&:hover:not(:disabled) ${StyledToolButtonInner}`]: {
|
||||
backgroundColor: '$hover',
|
||||
border: '1px solid $panel',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
1
packages/tldraw/src/components/ToolButton/index.ts
Normal file
1
packages/tldraw/src/components/ToolButton/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './ToolButton'
|
289
packages/tldraw/src/components/ToolsPanel/ActionButton.tsx
Normal file
289
packages/tldraw/src/components/ToolsPanel/ActionButton.tsx
Normal file
|
@ -0,0 +1,289 @@
|
|||
import * as React from 'react'
|
||||
import { Tooltip } from '~components/Tooltip/Tooltip'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import styled from '~styles'
|
||||
import { AlignType, Data, DistributeType, StretchType } from '~types'
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
AspectRatioIcon,
|
||||
CopyIcon,
|
||||
DotsHorizontalIcon,
|
||||
GroupIcon,
|
||||
LockClosedIcon,
|
||||
LockOpen1Icon,
|
||||
PinBottomIcon,
|
||||
PinTopIcon,
|
||||
RotateCounterClockwiseIcon,
|
||||
AlignBottomIcon,
|
||||
AlignCenterHorizontallyIcon,
|
||||
AlignCenterVerticallyIcon,
|
||||
AlignLeftIcon,
|
||||
AlignRightIcon,
|
||||
AlignTopIcon,
|
||||
SpaceEvenlyHorizontallyIcon,
|
||||
SpaceEvenlyVerticallyIcon,
|
||||
StretchHorizontallyIcon,
|
||||
StretchVerticallyIcon,
|
||||
} from '@radix-ui/react-icons'
|
||||
import { DMContent } from '~components/DropdownMenu'
|
||||
import { Divider } from '~components/Divider'
|
||||
import { TrashIcon } from '~components/icons'
|
||||
import { IconButton } from '~components/IconButton'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
|
||||
const selectedShapesCountSelector = (s: Data) =>
|
||||
s.document.pageStates[s.appState.currentPageId].selectedIds.length
|
||||
|
||||
const isAllLockedSelector = (s: Data) => {
|
||||
const page = s.document.pages[s.appState.currentPageId]
|
||||
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
||||
return selectedIds.every((id) => page.shapes[id].isLocked)
|
||||
}
|
||||
|
||||
const isAllAspectLockedSelector = (s: Data) => {
|
||||
const page = s.document.pages[s.appState.currentPageId]
|
||||
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
||||
return selectedIds.every((id) => page.shapes[id].isAspectRatioLocked)
|
||||
}
|
||||
|
||||
const isAllGroupedSelector = (s: Data) => {
|
||||
const page = s.document.pages[s.appState.currentPageId]
|
||||
const selectedShapes = s.document.pageStates[s.appState.currentPageId].selectedIds.map(
|
||||
(id) => page.shapes[id]
|
||||
)
|
||||
|
||||
return selectedShapes.every(
|
||||
(shape) =>
|
||||
shape.children !== undefined ||
|
||||
(shape.parentId === selectedShapes[0].parentId &&
|
||||
selectedShapes[0].parentId !== s.appState.currentPageId)
|
||||
)
|
||||
}
|
||||
|
||||
const hasSelectionSelector = (s: Data) => {
|
||||
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
||||
return selectedIds.length > 0
|
||||
}
|
||||
|
||||
const hasMultipleSelectionSelector = (s: Data) => {
|
||||
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
||||
return selectedIds.length > 1
|
||||
}
|
||||
|
||||
export function ActionButton(): JSX.Element {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
|
||||
const isAllLocked = useSelector(isAllLockedSelector)
|
||||
|
||||
const isAllAspectLocked = useSelector(isAllAspectLockedSelector)
|
||||
|
||||
const isAllGrouped = useSelector(isAllGroupedSelector)
|
||||
|
||||
const hasSelection = useSelector(hasSelectionSelector)
|
||||
|
||||
const hasMultipleSelection = useSelector(hasMultipleSelectionSelector)
|
||||
|
||||
const handleRotate = React.useCallback(() => {
|
||||
tlstate.rotate()
|
||||
}, [tlstate])
|
||||
|
||||
const handleDuplicate = React.useCallback(() => {
|
||||
tlstate.duplicate()
|
||||
}, [tlstate])
|
||||
|
||||
const handleToggleLocked = React.useCallback(() => {
|
||||
tlstate.toggleLocked()
|
||||
}, [tlstate])
|
||||
|
||||
const handleToggleAspectRatio = React.useCallback(() => {
|
||||
tlstate.toggleAspectRatioLocked()
|
||||
}, [tlstate])
|
||||
|
||||
const handleGroup = React.useCallback(() => {
|
||||
tlstate.group()
|
||||
}, [tlstate])
|
||||
|
||||
const handleMoveToBack = React.useCallback(() => {
|
||||
tlstate.moveToBack()
|
||||
}, [tlstate])
|
||||
|
||||
const handleMoveBackward = React.useCallback(() => {
|
||||
tlstate.moveBackward()
|
||||
}, [tlstate])
|
||||
|
||||
const handleMoveForward = React.useCallback(() => {
|
||||
tlstate.moveForward()
|
||||
}, [tlstate])
|
||||
|
||||
const handleMoveToFront = React.useCallback(() => {
|
||||
tlstate.moveToFront()
|
||||
}, [tlstate])
|
||||
|
||||
const handleDelete = React.useCallback(() => {
|
||||
tlstate.delete()
|
||||
}, [tlstate])
|
||||
|
||||
const alignTop = React.useCallback(() => {
|
||||
tlstate.align(AlignType.Top)
|
||||
}, [tlstate])
|
||||
|
||||
const alignCenterVertical = React.useCallback(() => {
|
||||
tlstate.align(AlignType.CenterVertical)
|
||||
}, [tlstate])
|
||||
|
||||
const alignBottom = React.useCallback(() => {
|
||||
tlstate.align(AlignType.Bottom)
|
||||
}, [tlstate])
|
||||
|
||||
const stretchVertically = React.useCallback(() => {
|
||||
tlstate.stretch(StretchType.Vertical)
|
||||
}, [tlstate])
|
||||
|
||||
const distributeVertically = React.useCallback(() => {
|
||||
tlstate.distribute(DistributeType.Vertical)
|
||||
}, [tlstate])
|
||||
|
||||
const alignLeft = React.useCallback(() => {
|
||||
tlstate.align(AlignType.Left)
|
||||
}, [tlstate])
|
||||
|
||||
const alignCenterHorizontal = React.useCallback(() => {
|
||||
tlstate.align(AlignType.CenterHorizontal)
|
||||
}, [tlstate])
|
||||
|
||||
const alignRight = React.useCallback(() => {
|
||||
tlstate.align(AlignType.Right)
|
||||
}, [tlstate])
|
||||
|
||||
const stretchHorizontally = React.useCallback(() => {
|
||||
tlstate.stretch(StretchType.Horizontal)
|
||||
}, [tlstate])
|
||||
|
||||
const distributeHorizontally = React.useCallback(() => {
|
||||
tlstate.distribute(DistributeType.Horizontal)
|
||||
}, [tlstate])
|
||||
|
||||
const selectedShapesCount = useSelector(selectedShapesCountSelector)
|
||||
|
||||
const hasTwoOrMore = selectedShapesCount > 1
|
||||
const hasThreeOrMore = selectedShapesCount > 2
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root dir="ltr">
|
||||
<DropdownMenu.Trigger dir="ltr" asChild>
|
||||
<ToolButton variant="circle">
|
||||
<DotsHorizontalIcon />
|
||||
</ToolButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DMContent>
|
||||
<>
|
||||
<ButtonsRow>
|
||||
<IconButton disabled={!hasSelection} onSelect={handleDuplicate}>
|
||||
<Tooltip label="Duplicate" kbd={`#D`}>
|
||||
<CopyIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasSelection} onSelect={handleRotate}>
|
||||
<Tooltip label="Rotate">
|
||||
<RotateCounterClockwiseIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasSelection} onSelect={handleToggleLocked}>
|
||||
<Tooltip label="Toogle Locked" kbd={`#L`}>
|
||||
{isAllLocked ? <LockClosedIcon /> : <LockOpen1Icon opacity={0.4} />}
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasSelection} onSelect={handleToggleAspectRatio}>
|
||||
<Tooltip label="Toogle Aspect Ratio Lock">
|
||||
<AspectRatioIcon opacity={isAllAspectLocked ? 1 : 0.4} />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<IconButton disabled={!isAllGrouped && !hasMultipleSelection} onSelect={handleGroup}>
|
||||
<Tooltip label="Group" kbd={`#G`}>
|
||||
<GroupIcon opacity={isAllGrouped ? 1 : 0.4} />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</ButtonsRow>
|
||||
<ButtonsRow>
|
||||
<IconButton disabled={!hasSelection} onSelect={handleMoveToBack}>
|
||||
<Tooltip label="Move to Back" kbd={`#⇧[`}>
|
||||
<PinBottomIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
|
||||
<IconButton disabled={!hasSelection} onSelect={handleMoveBackward}>
|
||||
<Tooltip label="Move Backward" kbd={`#[`}>
|
||||
<ArrowDownIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasSelection} onSelect={handleMoveForward}>
|
||||
<Tooltip label="Move Forward" kbd={`#]`}>
|
||||
<ArrowUpIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasSelection} onSelect={handleMoveToFront}>
|
||||
<Tooltip label="More to Front" kbd={`#⇧]`}>
|
||||
<PinTopIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasSelection} onSelect={handleDelete}>
|
||||
<Tooltip label="Delete" kbd="⌫">
|
||||
<TrashIcon />
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
</ButtonsRow>
|
||||
<Divider />
|
||||
<ButtonsRow>
|
||||
<IconButton disabled={!hasTwoOrMore} onSelect={alignLeft}>
|
||||
<AlignLeftIcon />
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasTwoOrMore} onSelect={alignCenterHorizontal}>
|
||||
<AlignCenterHorizontallyIcon />
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasTwoOrMore} onSelect={alignRight}>
|
||||
<AlignRightIcon />
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasTwoOrMore} onSelect={stretchHorizontally}>
|
||||
<StretchHorizontallyIcon />
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasThreeOrMore} onSelect={distributeHorizontally}>
|
||||
<SpaceEvenlyHorizontallyIcon />
|
||||
</IconButton>
|
||||
</ButtonsRow>
|
||||
<ButtonsRow>
|
||||
<IconButton disabled={!hasTwoOrMore} onSelect={alignTop}>
|
||||
<AlignTopIcon />
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasTwoOrMore} onSelect={alignCenterVertical}>
|
||||
<AlignCenterVerticallyIcon />
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasTwoOrMore} onSelect={alignBottom}>
|
||||
<AlignBottomIcon />
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasTwoOrMore} onSelect={stretchVertically}>
|
||||
<StretchVerticallyIcon />
|
||||
</IconButton>
|
||||
<IconButton disabled={!hasThreeOrMore} onSelect={distributeVertically}>
|
||||
<SpaceEvenlyVerticallyIcon />
|
||||
</IconButton>
|
||||
</ButtonsRow>
|
||||
</>
|
||||
</DMContent>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export const ButtonsRow = styled('div', {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
outline: 'none',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
padding: 0,
|
||||
})
|
|
@ -1,8 +1,9 @@
|
|||
import * as React from 'react'
|
||||
import { floatingContainer, rowButton } from '~components/shared'
|
||||
import css from '~styles'
|
||||
import styled from '~styles'
|
||||
import type { Data } from '~types'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { RowButton } from '~components/RowButton'
|
||||
import { MenuContent } from '~components/MenuContent'
|
||||
|
||||
const isEmptyCanvasSelector = (s: Data) =>
|
||||
Object.keys(s.document.pages[s.appState.currentPageId].shapes).length > 0 &&
|
||||
|
@ -16,17 +17,16 @@ export const BackToContent = React.memo(() => {
|
|||
if (!isEmptyCanvas) return null
|
||||
|
||||
return (
|
||||
<div className={backToContentButton()}>
|
||||
<button className={rowButton()} onClick={tlstate.zoomToContent}>
|
||||
Back to content
|
||||
</button>
|
||||
</div>
|
||||
<BackToContentContainer>
|
||||
<RowButton onSelect={tlstate.zoomToContent}>Back to content</RowButton>
|
||||
</BackToContentContainer>
|
||||
)
|
||||
})
|
||||
|
||||
const backToContentButton = css(floatingContainer, {
|
||||
const BackToContentContainer = styled(MenuContent, {
|
||||
pointerEvents: 'all',
|
||||
width: 'fit-content',
|
||||
minWidth: 0,
|
||||
gridRow: 1,
|
||||
flexGrow: 2,
|
||||
display: 'block',
|
22
packages/tldraw/src/components/ToolsPanel/LockButton.tsx
Normal file
22
packages/tldraw/src/components/ToolsPanel/LockButton.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import * as React from 'react'
|
||||
import { LockClosedIcon, LockOpen1Icon } from '@radix-ui/react-icons'
|
||||
import { Tooltip } from '~components/Tooltip'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
import type { Data } from '~types'
|
||||
|
||||
const isToolLockedSelector = (s: Data) => s.appState.isToolLocked
|
||||
|
||||
export function LockButton(): JSX.Element {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
|
||||
const isToolLocked = useSelector(isToolLockedSelector)
|
||||
|
||||
return (
|
||||
<Tooltip label="Lock Tool" kbd="7">
|
||||
<ToolButton variant="circle" isActive={isToolLocked} onSelect={tlstate.toggleToolLock}>
|
||||
{isToolLocked ? <LockClosedIcon /> : <LockOpen1Icon />}
|
||||
</ToolButton>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
|
@ -2,6 +2,7 @@ import * as React from 'react'
|
|||
import {
|
||||
ArrowTopRightIcon,
|
||||
CircleIcon,
|
||||
CursorArrowIcon,
|
||||
Pencil1Icon,
|
||||
Pencil2Icon,
|
||||
SquareIcon,
|
||||
|
@ -9,8 +10,8 @@ import {
|
|||
} from '@radix-ui/react-icons'
|
||||
import { Data, TLDrawShapeType } from '~types'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { floatingContainer } from '~components/shared'
|
||||
import { PrimaryButton } from '~components/tools-panel/styled'
|
||||
import { ToolButtonWithTooltip } from '~components/ToolButton'
|
||||
import { Panel } from '~components/Panel'
|
||||
|
||||
const activeToolSelector = (s: Data) => s.appState.activeTool
|
||||
|
||||
|
@ -19,6 +20,10 @@ export const PrimaryTools = React.memo((): JSX.Element => {
|
|||
|
||||
const activeTool = useSelector(activeToolSelector)
|
||||
|
||||
const selectSelectTool = React.useCallback(() => {
|
||||
tlstate.selectTool('select')
|
||||
}, [tlstate])
|
||||
|
||||
const selectDrawTool = React.useCallback(() => {
|
||||
tlstate.selectTool(TLDrawShapeType.Draw)
|
||||
}, [tlstate])
|
||||
|
@ -44,55 +49,63 @@ export const PrimaryTools = React.memo((): JSX.Element => {
|
|||
}, [tlstate])
|
||||
|
||||
return (
|
||||
<div className={floatingContainer()}>
|
||||
<PrimaryButton
|
||||
<Panel side="center">
|
||||
<ToolButtonWithTooltip
|
||||
kbd={'2'}
|
||||
label={'select'}
|
||||
onSelect={selectSelectTool}
|
||||
isActive={activeTool === 'select'}
|
||||
>
|
||||
<CursorArrowIcon />
|
||||
</ToolButtonWithTooltip>
|
||||
<ToolButtonWithTooltip
|
||||
kbd={'2'}
|
||||
label={TLDrawShapeType.Draw}
|
||||
onClick={selectDrawTool}
|
||||
onSelect={selectDrawTool}
|
||||
isActive={activeTool === TLDrawShapeType.Draw}
|
||||
>
|
||||
<Pencil1Icon />
|
||||
</PrimaryButton>
|
||||
<PrimaryButton
|
||||
</ToolButtonWithTooltip>
|
||||
<ToolButtonWithTooltip
|
||||
kbd={'3'}
|
||||
label={TLDrawShapeType.Rectangle}
|
||||
onClick={selectRectangleTool}
|
||||
onSelect={selectRectangleTool}
|
||||
isActive={activeTool === TLDrawShapeType.Rectangle}
|
||||
>
|
||||
<SquareIcon />
|
||||
</PrimaryButton>
|
||||
<PrimaryButton
|
||||
</ToolButtonWithTooltip>
|
||||
<ToolButtonWithTooltip
|
||||
kbd={'4'}
|
||||
label={TLDrawShapeType.Draw}
|
||||
onClick={selectEllipseTool}
|
||||
onSelect={selectEllipseTool}
|
||||
isActive={activeTool === TLDrawShapeType.Ellipse}
|
||||
>
|
||||
<CircleIcon />
|
||||
</PrimaryButton>
|
||||
<PrimaryButton
|
||||
</ToolButtonWithTooltip>
|
||||
<ToolButtonWithTooltip
|
||||
kbd={'5'}
|
||||
label={TLDrawShapeType.Arrow}
|
||||
onClick={selectArrowTool}
|
||||
onSelect={selectArrowTool}
|
||||
isActive={activeTool === TLDrawShapeType.Arrow}
|
||||
>
|
||||
<ArrowTopRightIcon />
|
||||
</PrimaryButton>
|
||||
<PrimaryButton
|
||||
</ToolButtonWithTooltip>
|
||||
<ToolButtonWithTooltip
|
||||
kbd={'6'}
|
||||
label={TLDrawShapeType.Text}
|
||||
onClick={selectTextTool}
|
||||
onSelect={selectTextTool}
|
||||
isActive={activeTool === TLDrawShapeType.Text}
|
||||
>
|
||||
<TextIcon />
|
||||
</PrimaryButton>
|
||||
<PrimaryButton
|
||||
</ToolButtonWithTooltip>
|
||||
<ToolButtonWithTooltip
|
||||
kbd={'7'}
|
||||
label={TLDrawShapeType.Sticky}
|
||||
onClick={selectStickyTool}
|
||||
onSelect={selectStickyTool}
|
||||
isActive={activeTool === TLDrawShapeType.Sticky}
|
||||
>
|
||||
<Pencil2Icon />
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</ToolButtonWithTooltip>
|
||||
</Panel>
|
||||
)
|
||||
})
|
|
@ -1,7 +1,8 @@
|
|||
import * as React from 'react'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import type { Data } from '~types'
|
||||
import css from '~styles'
|
||||
import styled from '~styles'
|
||||
import { breakpoints } from '~components/breakpoints'
|
||||
|
||||
const statusSelector = (s: Data) => s.appState.status
|
||||
const activeToolSelector = (s: Data) => s.appState.activeTool
|
||||
|
@ -12,15 +13,15 @@ export function StatusBar(): JSX.Element | null {
|
|||
const activeTool = useSelector(activeToolSelector)
|
||||
|
||||
return (
|
||||
<div className={statusBarContainer({ size: { '@sm': 'small' } })}>
|
||||
<div className={section()}>
|
||||
<StyledStatusBar bp={breakpoints}>
|
||||
<StyledSection>
|
||||
{activeTool} | {status}
|
||||
</div>
|
||||
</div>
|
||||
</StyledSection>
|
||||
</StyledStatusBar>
|
||||
)
|
||||
}
|
||||
|
||||
const statusBarContainer = css({
|
||||
const StyledStatusBar = styled('div', {
|
||||
height: 40,
|
||||
userSelect: 'none',
|
||||
borderTop: '1px solid $border',
|
||||
|
@ -36,7 +37,7 @@ const statusBarContainer = css({
|
|||
padding: '0 16px',
|
||||
|
||||
variants: {
|
||||
size: {
|
||||
bp: {
|
||||
small: {
|
||||
fontSize: '$1',
|
||||
},
|
||||
|
@ -44,7 +45,7 @@ const statusBarContainer = css({
|
|||
},
|
||||
})
|
||||
|
||||
const section = css({
|
||||
const StyledSection = styled('div', {
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
})
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from 'react'
|
||||
import { ToolsPanel } from './tools-panel'
|
||||
import { ToolsPanel } from './ToolsPanel'
|
||||
import { renderWithContext } from '~test'
|
||||
|
||||
describe('tools panel', () => {
|
78
packages/tldraw/src/components/ToolsPanel/ToolsPanel.tsx
Normal file
78
packages/tldraw/src/components/ToolsPanel/ToolsPanel.tsx
Normal file
|
@ -0,0 +1,78 @@
|
|||
import * as React from 'react'
|
||||
import styled from '~styles'
|
||||
import type { Data } from '~types'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { StatusBar } from './StatusBar'
|
||||
import { BackToContent } from './BackToContent'
|
||||
import { PrimaryTools } from './PrimaryTools'
|
||||
import { ActionButton } from './ActionButton'
|
||||
import { LockButton } from './LockButton'
|
||||
|
||||
const isDebugModeSelector = (s: Data) => s.settings.isDebugMode
|
||||
|
||||
export const ToolsPanel = React.memo((): JSX.Element => {
|
||||
const { useSelector } = useTLDrawContext()
|
||||
|
||||
const isDebugMode = useSelector(isDebugModeSelector)
|
||||
|
||||
return (
|
||||
<StyledToolsPanelContainer>
|
||||
<StyledCenterWrap>
|
||||
<BackToContent />
|
||||
<StyledPrimaryTools>
|
||||
<ActionButton />
|
||||
<PrimaryTools />
|
||||
<LockButton />
|
||||
</StyledPrimaryTools>
|
||||
</StyledCenterWrap>
|
||||
{isDebugMode && (
|
||||
<StyledStatusWrap>
|
||||
<StatusBar />
|
||||
</StyledStatusWrap>
|
||||
)}
|
||||
</StyledToolsPanelContainer>
|
||||
)
|
||||
})
|
||||
|
||||
const StyledToolsPanelContainer = styled('div', {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
width: '100%',
|
||||
minWidth: 0,
|
||||
maxWidth: '100%',
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'auto auto auto',
|
||||
gridTemplateRows: 'auto auto',
|
||||
justifyContent: 'space-between',
|
||||
padding: '0',
|
||||
alignItems: 'flex-end',
|
||||
zIndex: 200,
|
||||
pointerEvents: 'none',
|
||||
'& > div > *': {
|
||||
pointerEvents: 'all',
|
||||
},
|
||||
})
|
||||
|
||||
const StyledCenterWrap = styled('div', {
|
||||
gridRow: 1,
|
||||
gridColumn: 2,
|
||||
display: 'flex',
|
||||
width: 'fit-content',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'column',
|
||||
gap: 12,
|
||||
})
|
||||
|
||||
const StyledStatusWrap = styled('div', {
|
||||
gridRow: 2,
|
||||
gridColumn: '1 / span 3',
|
||||
})
|
||||
|
||||
const StyledPrimaryTools = styled('div', {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
gap: '$2',
|
||||
})
|
1
packages/tldraw/src/components/ToolsPanel/index.ts
Normal file
1
packages/tldraw/src/components/ToolsPanel/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './ToolsPanel'
|
|
@ -1,7 +1,7 @@
|
|||
import * as RadixTooltip from '@radix-ui/react-tooltip'
|
||||
import * as React from 'react'
|
||||
import css from '~styles'
|
||||
import { Kbd } from './kbd'
|
||||
import { Kbd } from '~components/Kbd'
|
||||
import styled from '~styles'
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Tooltip */
|
||||
|
@ -25,22 +25,16 @@ export function Tooltip({
|
|||
<RadixTooltip.Trigger asChild={true}>
|
||||
<span>{children}</span>
|
||||
</RadixTooltip.Trigger>
|
||||
<RadixTooltip.Content className={content()} side={side} sideOffset={8}>
|
||||
<StyledContent side={side} sideOffset={8}>
|
||||
{label}
|
||||
{kbdProp ? <Kbd variant="tooltip">{kbdProp}</Kbd> : null}
|
||||
<RadixTooltip.Arrow className={arrow()} />
|
||||
</RadixTooltip.Content>
|
||||
<StyledArrow />
|
||||
</StyledContent>
|
||||
</RadixTooltip.Root>
|
||||
)
|
||||
}
|
||||
|
||||
const button = css({
|
||||
border: 'none',
|
||||
background: 'none',
|
||||
padding: 0,
|
||||
})
|
||||
|
||||
const content = css({
|
||||
const StyledContent = styled(RadixTooltip.Content, {
|
||||
borderRadius: 3,
|
||||
padding: '$3 $3 $3 $3',
|
||||
fontSize: '$1',
|
||||
|
@ -53,7 +47,7 @@ const content = css({
|
|||
userSelect: 'none',
|
||||
})
|
||||
|
||||
const arrow = css({
|
||||
const StyledArrow = styled(RadixTooltip.Arrow, {
|
||||
fill: '$tooltipBg',
|
||||
margin: '0 8px',
|
||||
})
|
1
packages/tldraw/src/components/Tooltip/index.ts
Normal file
1
packages/tldraw/src/components/Tooltip/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './Tooltip'
|
43
packages/tldraw/src/components/TopPanel/ColorMenu.tsx
Normal file
43
packages/tldraw/src/components/TopPanel/ColorMenu.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import * as React from 'react'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { strokes } from '~shape-utils'
|
||||
import { useTheme, useTLDrawContext } from '~hooks'
|
||||
import type { Data, ColorStyle } from '~types'
|
||||
import CircleIcon from '~components/icons/CircleIcon'
|
||||
import { DMContent, DMRadioItem, DMTriggerIcon } from '~components/DropdownMenu'
|
||||
import { BoxIcon } from '~components/icons'
|
||||
import { IconButton } from '~components/IconButton'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
import { Tooltip } from '~components/Tooltip'
|
||||
|
||||
const selectColor = (s: Data) => s.appState.selectedStyle.color
|
||||
|
||||
export const ColorMenu = React.memo((): JSX.Element => {
|
||||
const { theme } = useTheme()
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
|
||||
const color = useSelector(selectColor)
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root dir="ltr">
|
||||
<DMTriggerIcon>
|
||||
<CircleIcon size={16} fill={strokes[theme][color]} stroke={strokes[theme][color]} />
|
||||
</DMTriggerIcon>
|
||||
<DMContent variant="grid">
|
||||
{Object.keys(strokes[theme]).map((colorStyle: string) => (
|
||||
<ToolButton
|
||||
key={colorStyle}
|
||||
variant="icon"
|
||||
isActive={color === colorStyle}
|
||||
onSelect={() => tlstate.style({ color: colorStyle as ColorStyle })}
|
||||
>
|
||||
<BoxIcon
|
||||
fill={strokes[theme][colorStyle as ColorStyle]}
|
||||
stroke={strokes[theme][colorStyle as ColorStyle]}
|
||||
/>
|
||||
</ToolButton>
|
||||
))}
|
||||
</DMContent>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
})
|
100
packages/tldraw/src/components/TopPanel/DashMenu.tsx
Normal file
100
packages/tldraw/src/components/TopPanel/DashMenu.tsx
Normal file
|
@ -0,0 +1,100 @@
|
|||
import * as React from 'react'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { DashStyle, Data } from '~types'
|
||||
import { DMContent, DMRadioItem, DMTriggerIcon } from '~components/DropdownMenu'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
import { Tooltip } from '~components/Tooltip'
|
||||
|
||||
const dashes = {
|
||||
[DashStyle.Draw]: <DashDrawIcon />,
|
||||
[DashStyle.Solid]: <DashSolidIcon />,
|
||||
[DashStyle.Dashed]: <DashDashedIcon />,
|
||||
[DashStyle.Dotted]: <DashDottedIcon />,
|
||||
}
|
||||
|
||||
const selectDash = (s: Data) => s.appState.selectedStyle.dash
|
||||
|
||||
export const DashMenu = React.memo((): JSX.Element => {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
|
||||
const dash = useSelector(selectDash)
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root dir="ltr">
|
||||
<DMTriggerIcon>{dashes[dash]}</DMTriggerIcon>
|
||||
<DMContent>
|
||||
{Object.keys(DashStyle).map((dashStyle) => (
|
||||
<ToolButton
|
||||
key={dashStyle}
|
||||
variant="icon"
|
||||
isActive={dash === dashStyle}
|
||||
onSelect={() => tlstate.style({ dash: dashStyle as DashStyle })}
|
||||
>
|
||||
{dashes[dashStyle as DashStyle]}
|
||||
</ToolButton>
|
||||
))}
|
||||
</DMContent>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
})
|
||||
|
||||
function DashSolidIcon(): JSX.Element {
|
||||
return (
|
||||
<svg width="24" height="24" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx={12} cy={12} r={8} fill="none" strokeWidth={2} strokeLinecap="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function DashDashedIcon(): JSX.Element {
|
||||
return (
|
||||
<svg width="24" height="24" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle
|
||||
cx={12}
|
||||
cy={12}
|
||||
r={8}
|
||||
fill="none"
|
||||
strokeWidth={2.5}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={50.26548 * 0.1}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
const dottedDasharray = `${50.26548 * 0.025} ${50.26548 * 0.1}`
|
||||
|
||||
function DashDottedIcon(): JSX.Element {
|
||||
return (
|
||||
<svg width="24" height="24" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle
|
||||
cx={12}
|
||||
cy={12}
|
||||
r={8}
|
||||
fill="none"
|
||||
strokeWidth={2.5}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={dottedDasharray}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function DashDrawIcon(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="1 1.5 21 22"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.0162 19.2768C10.0162 19.2768 9.90679 19.2517 9.6879 19.2017C9.46275 19.1454 9.12816 19.0422 8.68413 18.8921C8.23384 18.7358 7.81482 18.545 7.42707 18.3199C7.03307 18.101 6.62343 17.7883 6.19816 17.3818C5.77289 16.9753 5.33511 16.3718 4.88482 15.5713C4.43453 14.7645 4.1531 13.8545 4.04053 12.8414C3.92795 11.822 4.04991 10.8464 4.40639 9.91451C4.76286 8.98266 5.39452 8.10084 6.30135 7.26906C7.21444 6.44353 8.29325 5.83377 9.5378 5.43976C10.7823 5.05202 11.833 4.92068 12.6898 5.04576C13.5466 5.16459 14.3878 5.43664 15.2133 5.86191C16.0388 6.28718 16.7768 6.8688 17.4272 7.60678C18.0714 8.34475 18.5404 9.21406 18.8344 10.2147C19.1283 11.2153 19.1721 12.2598 18.9657 13.348C18.7593 14.4299 18.2872 15.4337 17.5492 16.3593C16.8112 17.2849 15.9263 18.0072 14.8944 18.5263C13.8624 19.0391 12.9056 19.3174 12.0238 19.3612C11.142 19.405 10.2101 19.2705 9.22823 18.9578C8.24635 18.6451 7.35828 18.151 6.56402 17.4756C5.77601 16.8002 6.08871 16.8658 7.50212 17.6726C8.90927 18.4731 10.1444 18.8484 11.2076 18.7983C12.2645 18.7545 13.2965 18.4825 14.3034 17.9822C15.3102 17.4819 16.1264 16.8221 16.7518 16.0028C17.3772 15.1835 17.7681 14.3111 17.9244 13.3855C18.0808 12.4599 18.0401 11.5781 17.8025 10.74C17.5586 9.902 17.1739 9.15464 16.6486 8.49797C16.1233 7.8413 15.2289 7.27844 13.9656 6.80939C12.7086 6.34034 11.4203 6.20901 10.1007 6.41539C8.78732 6.61552 7.69599 7.06893 6.82669 7.77564C5.96363 8.48859 5.34761 9.26409 4.97863 10.1021C4.60964 10.9402 4.45329 11.8376 4.50958 12.7945C4.56586 13.7513 4.79101 14.6238 5.18501 15.4118C5.57276 16.1998 5.96363 16.8002 6.35764 17.2129C6.75164 17.6257 7.13313 17.9509 7.50212 18.1886C7.87736 18.4325 8.28074 18.642 8.71227 18.8171C9.15005 18.9922 9.47839 19.111 9.69728 19.1736C9.91617 19.2361 10.0256 19.2705 10.0256 19.2768H10.0162Z"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
31
packages/tldraw/src/components/TopPanel/FillCheckbox.tsx
Normal file
31
packages/tldraw/src/components/TopPanel/FillCheckbox.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import * as React from 'react'
|
||||
import * as Checkbox from '@radix-ui/react-checkbox'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import type { Data } from '~types'
|
||||
import { Tooltip } from '~components/Tooltip'
|
||||
import { BoxIcon, IsFilledIcon } from '~components/icons'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
|
||||
const isFilledSelector = (s: Data) => s.appState.selectedStyle.isFilled
|
||||
|
||||
export const FillCheckbox = React.memo((): JSX.Element => {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
|
||||
const isFilled = useSelector(isFilledSelector)
|
||||
|
||||
const handleIsFilledChange = React.useCallback(
|
||||
(isFilled: boolean) => tlstate.style({ isFilled }),
|
||||
[tlstate]
|
||||
)
|
||||
|
||||
return (
|
||||
<Checkbox.Root dir="ltr" asChild checked={isFilled} onCheckedChange={handleIsFilledChange}>
|
||||
<ToolButton variant="icon">
|
||||
<BoxIcon />
|
||||
<Checkbox.Indicator>
|
||||
<IsFilledIcon />
|
||||
</Checkbox.Indicator>
|
||||
</ToolButton>
|
||||
</Checkbox.Root>
|
||||
)
|
||||
})
|
111
packages/tldraw/src/components/TopPanel/Menu.tsx
Normal file
111
packages/tldraw/src/components/TopPanel/Menu.tsx
Normal file
|
@ -0,0 +1,111 @@
|
|||
import * as React from 'react'
|
||||
import { ExitIcon, HamburgerMenuIcon } from '@radix-ui/react-icons'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { PreferencesMenu } from './PreferencesMenu'
|
||||
import { DMItem, DMContent, DMDivider, DMSubMenu, DMTriggerIcon } from '~components/DropdownMenu'
|
||||
import { SmallIcon } from '~components/SmallIcon'
|
||||
|
||||
export const Menu = React.memo(() => {
|
||||
const { tlstate } = useTLDrawContext()
|
||||
|
||||
const handleNew = React.useCallback(() => {
|
||||
if (window.confirm('Are you sure you want to start a new project?')) {
|
||||
tlstate.newProject()
|
||||
}
|
||||
}, [tlstate])
|
||||
|
||||
const handleSave = React.useCallback(() => {
|
||||
tlstate.saveProject()
|
||||
}, [tlstate])
|
||||
|
||||
const handleLoad = React.useCallback(() => {
|
||||
tlstate.loadProject()
|
||||
}, [tlstate])
|
||||
|
||||
const handleSignOut = React.useCallback(() => {
|
||||
tlstate.signOut()
|
||||
}, [tlstate])
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
tlstate.copy()
|
||||
}, [tlstate])
|
||||
|
||||
const handlePaste = React.useCallback(() => {
|
||||
tlstate.paste()
|
||||
}, [tlstate])
|
||||
|
||||
const handleCopySvg = React.useCallback(() => {
|
||||
tlstate.copySvg()
|
||||
}, [tlstate])
|
||||
|
||||
const handleCopyJson = React.useCallback(() => {
|
||||
tlstate.copyJson()
|
||||
}, [tlstate])
|
||||
|
||||
const handleSelectAll = React.useCallback(() => {
|
||||
tlstate.selectAll()
|
||||
}, [tlstate])
|
||||
|
||||
const handleDeselectAll = React.useCallback(() => {
|
||||
tlstate.deselectAll()
|
||||
}, [tlstate])
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DMTriggerIcon>
|
||||
<HamburgerMenuIcon />
|
||||
</DMTriggerIcon>
|
||||
<DMContent variant="menu">
|
||||
<DMSubMenu label="File...">
|
||||
<DMItem onSelect={handleNew} kbd="#N">
|
||||
New Project
|
||||
</DMItem>
|
||||
<DMItem disabled onSelect={handleLoad} kbd="#L">
|
||||
Open...
|
||||
</DMItem>
|
||||
<DMItem disabled onSelect={handleSave} kbd="#S">
|
||||
Save
|
||||
</DMItem>
|
||||
<DMItem disabled onSelect={handleSave} kbd="⇧#S">
|
||||
Save As...
|
||||
</DMItem>
|
||||
</DMSubMenu>
|
||||
<DMSubMenu label="Edit...">
|
||||
<DMItem onSelect={tlstate.undo} kbd="#Z">
|
||||
Undo
|
||||
</DMItem>
|
||||
<DMItem onSelect={tlstate.redo} kbd="#⇧Z">
|
||||
Redo
|
||||
</DMItem>
|
||||
<DMDivider dir="ltr" />
|
||||
<DMItem onSelect={handleCopy} kbd="#C">
|
||||
Copy
|
||||
</DMItem>
|
||||
<DMItem onSelect={handlePaste} kbd="#V">
|
||||
Paste
|
||||
</DMItem>
|
||||
<DMDivider dir="ltr" />
|
||||
<DMItem onSelect={handleCopySvg} kbd="#⇧C">
|
||||
Copy as SVG
|
||||
</DMItem>
|
||||
<DMItem onSelect={handleCopyJson}>Copy as JSON</DMItem>
|
||||
<DMDivider dir="ltr" />
|
||||
<DMItem onSelect={handleSelectAll} kbd="#A">
|
||||
Select All
|
||||
</DMItem>
|
||||
<DMItem onSelect={handleDeselectAll}>Select None</DMItem>
|
||||
</DMSubMenu>
|
||||
<DMDivider dir="ltr" />
|
||||
<PreferencesMenu />
|
||||
<DMDivider dir="ltr" />
|
||||
<DMItem disabled onSelect={handleSignOut}>
|
||||
Sign Out
|
||||
<SmallIcon>
|
||||
<ExitIcon />
|
||||
</SmallIcon>
|
||||
</DMItem>
|
||||
</DMContent>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
})
|
|
@ -1,19 +1,14 @@
|
|||
import * as React from 'react'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { PlusIcon, CheckIcon } from '@radix-ui/react-icons'
|
||||
import {
|
||||
breakpoints,
|
||||
DropdownMenuButton,
|
||||
DropdownMenuDivider,
|
||||
rowButton,
|
||||
menuContent,
|
||||
floatingContainer,
|
||||
iconWrapper,
|
||||
} from '~components/shared'
|
||||
import { PageOptionsDialog } from '~components/page-options-dialog'
|
||||
import css from '~styles'
|
||||
import { PageOptionsDialog } from './PageOptionsDialog'
|
||||
import styled from '~styles'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import type { Data } from '~types'
|
||||
import { DMContent, DMDivider } from '~components/DropdownMenu'
|
||||
import { SmallIcon } from '~components/SmallIcon'
|
||||
import { RowButton } from '~components/RowButton'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
|
||||
const sortedSelector = (s: Data) =>
|
||||
Object.values(s.document.pages).sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0))
|
||||
|
@ -22,7 +17,7 @@ const currentPageNameSelector = (s: Data) => s.document.pages[s.appState.current
|
|||
|
||||
const currentPageIdSelector = (s: Data) => s.document.pages[s.appState.currentPageId].id
|
||||
|
||||
export function PagePanel(): JSX.Element {
|
||||
export function PageMenu(): JSX.Element {
|
||||
const { useSelector } = useTLDrawContext()
|
||||
|
||||
const rIsOpen = React.useRef(false)
|
||||
|
@ -51,14 +46,12 @@ export function PagePanel(): JSX.Element {
|
|||
|
||||
return (
|
||||
<DropdownMenu.Root dir="ltr" open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<div className={floatingContainer()}>
|
||||
<DropdownMenu.Trigger className={rowButton({ bp: breakpoints, variant: 'noIcon' })}>
|
||||
<span>{currentPageName || 'Page'}</span>
|
||||
</DropdownMenu.Trigger>
|
||||
</div>
|
||||
<DropdownMenu.Content className={menuContent()} sideOffset={8} align="start">
|
||||
<DropdownMenu.Trigger dir="ltr" asChild>
|
||||
<ToolButton variant="text">{currentPageName || 'Page'}</ToolButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DMContent variant="menu" align="start">
|
||||
{isOpen && <PageMenuContent onClose={handleClose} />}
|
||||
</DropdownMenu.Content>
|
||||
</DMContent>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
}
|
||||
|
@ -86,34 +79,40 @@ function PageMenuContent({ onClose }: { onClose: () => void }) {
|
|||
<>
|
||||
<DropdownMenu.RadioGroup value={currentPageId} onValueChange={handleChangePage}>
|
||||
{sortedPages.map((page) => (
|
||||
<div className={buttonWithOptions()} key={page.id}>
|
||||
<ButtonWithOptions key={page.id}>
|
||||
<DropdownMenu.RadioItem
|
||||
className={rowButton({ bp: breakpoints, variant: 'pageButton' })}
|
||||
title={page.name || 'Page'}
|
||||
value={page.id}
|
||||
key={page.id}
|
||||
asChild
|
||||
>
|
||||
<span>{page.name || 'Page'}</span>
|
||||
<DropdownMenu.ItemIndicator>
|
||||
<div className={iconWrapper({ size: 'small' })}>
|
||||
<CheckIcon />
|
||||
</div>
|
||||
</DropdownMenu.ItemIndicator>
|
||||
<PageButton>
|
||||
<span>{page.name || 'Page'}</span>
|
||||
<DropdownMenu.ItemIndicator>
|
||||
<SmallIcon>
|
||||
<CheckIcon />
|
||||
</SmallIcon>
|
||||
</DropdownMenu.ItemIndicator>
|
||||
</PageButton>
|
||||
</DropdownMenu.RadioItem>
|
||||
<PageOptionsDialog page={page} onClose={onClose} />
|
||||
</div>
|
||||
</ButtonWithOptions>
|
||||
))}
|
||||
</DropdownMenu.RadioGroup>
|
||||
<DropdownMenuDivider />
|
||||
<DropdownMenuButton onSelect={handleCreatePage}>
|
||||
<span>Create Page</span>
|
||||
<div className={iconWrapper({ size: 'small' })}>
|
||||
<PlusIcon />
|
||||
</div>
|
||||
</DropdownMenuButton>
|
||||
<DMDivider />
|
||||
<DropdownMenu.Item onSelect={handleCreatePage} asChild>
|
||||
<RowButton>
|
||||
<span>Create Page</span>
|
||||
<SmallIcon>
|
||||
<PlusIcon />
|
||||
</SmallIcon>
|
||||
</RowButton>
|
||||
</DropdownMenu.Item>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const buttonWithOptions = css({
|
||||
const ButtonWithOptions = styled('div', {
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr auto',
|
||||
gridAutoFlow: 'column',
|
||||
|
@ -126,3 +125,7 @@ const buttonWithOptions = css({
|
|||
opacity: 1,
|
||||
},
|
||||
})
|
||||
|
||||
export const PageButton = styled(RowButton, {
|
||||
minWidth: 128,
|
||||
})
|
139
packages/tldraw/src/components/TopPanel/PageOptionsDialog.tsx
Normal file
139
packages/tldraw/src/components/TopPanel/PageOptionsDialog.tsx
Normal file
|
@ -0,0 +1,139 @@
|
|||
import * as React from 'react'
|
||||
import * as Dialog from '@radix-ui/react-alert-dialog'
|
||||
import { MixerVerticalIcon } from '@radix-ui/react-icons'
|
||||
import type { Data, TLDrawPage } from '~types'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { RowButton, RowButtonProps } from '~components/RowButton'
|
||||
import styled from '~styles'
|
||||
import { Divider } from '~components/Divider'
|
||||
import { IconButton } from '~components/IconButton/IconButton'
|
||||
import { SmallIcon } from '~components/SmallIcon'
|
||||
import { breakpoints } from '~components/breakpoints'
|
||||
|
||||
const canDeleteSelector = (s: Data) => {
|
||||
return Object.keys(s.document.pages).length > 1
|
||||
}
|
||||
|
||||
interface PageOptionsDialogProps {
|
||||
page: TLDrawPage
|
||||
onOpen?: () => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogProps): JSX.Element {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
|
||||
const canDelete = useSelector(canDeleteSelector)
|
||||
|
||||
const rInput = React.useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleDuplicate = React.useCallback(() => {
|
||||
tlstate.duplicatePage(page.id)
|
||||
onClose?.()
|
||||
}, [tlstate])
|
||||
|
||||
const handleDelete = React.useCallback(() => {
|
||||
if (window.confirm(`Are you sure you want to delete this page?`)) {
|
||||
tlstate.deletePage(page.id)
|
||||
onClose?.()
|
||||
}
|
||||
}, [tlstate])
|
||||
|
||||
const handleOpenChange = React.useCallback(
|
||||
(isOpen: boolean) => {
|
||||
setIsOpen(isOpen)
|
||||
|
||||
if (isOpen) {
|
||||
onOpen?.()
|
||||
return
|
||||
}
|
||||
},
|
||||
[tlstate, name]
|
||||
)
|
||||
|
||||
function stopPropagation(e: React.KeyboardEvent<HTMLDivElement>) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
// TODO: Replace with text input
|
||||
function handleRename() {
|
||||
const nextName = window.prompt('New name:', page.name)
|
||||
tlstate.renamePage(page.id, nextName || page.name || 'Page')
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isOpen) {
|
||||
requestAnimationFrame(() => {
|
||||
rInput.current?.focus()
|
||||
rInput.current?.select()
|
||||
})
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
return (
|
||||
<Dialog.Root open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<Dialog.Trigger asChild data-shy="true">
|
||||
<IconButton bp={breakpoints}>
|
||||
<SmallIcon>
|
||||
<MixerVerticalIcon />
|
||||
</SmallIcon>
|
||||
</IconButton>
|
||||
</Dialog.Trigger>
|
||||
<StyledDialogOverlay />
|
||||
<StyledDialogContent onKeyDown={stopPropagation} onKeyUp={stopPropagation}>
|
||||
<DialogAction onSelect={handleRename}>Rename</DialogAction>
|
||||
<DialogAction onSelect={handleDuplicate}>Duplicate</DialogAction>
|
||||
<DialogAction disabled={!canDelete} onSelect={handleDelete}>
|
||||
Delete
|
||||
</DialogAction>
|
||||
<Divider />
|
||||
<Dialog.Cancel asChild>
|
||||
<RowButton>Cancel</RowButton>
|
||||
</Dialog.Cancel>
|
||||
</StyledDialogContent>
|
||||
</Dialog.Root>
|
||||
)
|
||||
}
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Dialog */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
export const StyledDialogContent = styled(Dialog.Content, {
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
minWidth: 240,
|
||||
maxWidth: 'fit-content',
|
||||
maxHeight: '85vh',
|
||||
marginTop: '-5vh',
|
||||
pointerEvents: 'all',
|
||||
backgroundColor: '$panel',
|
||||
border: '1px solid $panelBorder',
|
||||
padding: '$0',
|
||||
borderRadius: '$2',
|
||||
font: '$ui',
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
})
|
||||
|
||||
export const StyledDialogOverlay = styled(Dialog.Overlay, {
|
||||
backgroundColor: 'rgba(0, 0, 0, .15)',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
})
|
||||
|
||||
function DialogAction({ onSelect, ...rest }: RowButtonProps) {
|
||||
return (
|
||||
<Dialog.Action asChild onClick={onSelect}>
|
||||
<RowButton {...rest} />
|
||||
</Dialog.Action>
|
||||
)
|
||||
}
|
70
packages/tldraw/src/components/TopPanel/PreferencesMenu.tsx
Normal file
70
packages/tldraw/src/components/TopPanel/PreferencesMenu.tsx
Normal file
|
@ -0,0 +1,70 @@
|
|||
import * as React from 'react'
|
||||
import { DMCheckboxItem, DMDivider, DMSubMenu } from '~components/DropdownMenu'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import type { Data } from '~types'
|
||||
|
||||
const settingsSelector = (s: Data) => s.settings
|
||||
|
||||
export function PreferencesMenu() {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
|
||||
const settings = useSelector(settingsSelector)
|
||||
|
||||
const toggleDebugMode = React.useCallback(() => {
|
||||
tlstate.setSetting('isDebugMode', (v) => !v)
|
||||
}, [tlstate])
|
||||
|
||||
const toggleDarkMode = React.useCallback(() => {
|
||||
tlstate.setSetting('isDarkMode', (v) => !v)
|
||||
}, [tlstate])
|
||||
|
||||
const toggleFocusMode = React.useCallback(() => {
|
||||
tlstate.setSetting('isFocusMode', (v) => !v)
|
||||
}, [tlstate])
|
||||
|
||||
const toggleRotateHandle = React.useCallback(() => {
|
||||
tlstate.setSetting('showRotateHandles', (v) => !v)
|
||||
}, [tlstate])
|
||||
|
||||
const toggleBoundShapesHandle = React.useCallback(() => {
|
||||
tlstate.setSetting('showBindingHandles', (v) => !v)
|
||||
}, [tlstate])
|
||||
|
||||
const toggleisSnapping = React.useCallback(() => {
|
||||
tlstate.setSetting('isSnapping', (v) => !v)
|
||||
}, [tlstate])
|
||||
|
||||
const toggleCloneControls = React.useCallback(() => {
|
||||
tlstate.setSetting('showCloneHandles', (v) => !v)
|
||||
}, [tlstate])
|
||||
|
||||
return (
|
||||
<DMSubMenu label="Preferences">
|
||||
<DMCheckboxItem checked={settings.isDarkMode} onCheckedChange={toggleDarkMode} kbd="#⇧D">
|
||||
Dark Mode
|
||||
</DMCheckboxItem>
|
||||
<DMCheckboxItem checked={settings.isFocusMode} onCheckedChange={toggleFocusMode} kbd="⇧.">
|
||||
Focus Mode
|
||||
</DMCheckboxItem>
|
||||
<DMCheckboxItem checked={settings.isDebugMode} onCheckedChange={toggleDebugMode}>
|
||||
Debug Mode
|
||||
</DMCheckboxItem>
|
||||
<DMDivider />
|
||||
<DMCheckboxItem checked={settings.showRotateHandles} onCheckedChange={toggleRotateHandle}>
|
||||
Rotate Handles
|
||||
</DMCheckboxItem>
|
||||
<DMCheckboxItem
|
||||
checked={settings.showBindingHandles}
|
||||
onCheckedChange={toggleBoundShapesHandle}
|
||||
>
|
||||
Binding Handles
|
||||
</DMCheckboxItem>
|
||||
<DMCheckboxItem checked={settings.showCloneHandles} onCheckedChange={toggleCloneControls}>
|
||||
Clone Handles
|
||||
</DMCheckboxItem>
|
||||
<DMCheckboxItem checked={settings.isSnapping} onCheckedChange={toggleisSnapping}>
|
||||
Always Show Snaps
|
||||
</DMCheckboxItem>
|
||||
</DMSubMenu>
|
||||
)
|
||||
}
|
39
packages/tldraw/src/components/TopPanel/SizeMenu.tsx
Normal file
39
packages/tldraw/src/components/TopPanel/SizeMenu.tsx
Normal file
|
@ -0,0 +1,39 @@
|
|||
import * as React from 'react'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { Data, SizeStyle } from '~types'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { DMContent, DMTriggerIcon } from '~components/DropdownMenu'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
import { SizeSmallIcon, SizeMediumIcon, SizeLargeIcon } from '~components/icons'
|
||||
import { Tooltip } from '~components/Tooltip'
|
||||
|
||||
const sizes = {
|
||||
[SizeStyle.Small]: <SizeSmallIcon />,
|
||||
[SizeStyle.Medium]: <SizeMediumIcon />,
|
||||
[SizeStyle.Large]: <SizeLargeIcon />,
|
||||
}
|
||||
|
||||
const selectSize = (s: Data) => s.appState.selectedStyle.size
|
||||
|
||||
export const SizeMenu = React.memo((): JSX.Element => {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
|
||||
const size = useSelector(selectSize)
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root dir="ltr">
|
||||
<DMTriggerIcon>{sizes[size as SizeStyle]}</DMTriggerIcon>
|
||||
<DMContent>
|
||||
{Object.keys(SizeStyle).map((sizeStyle: string) => (
|
||||
<ToolButton
|
||||
key={sizeStyle}
|
||||
isActive={size === sizeStyle}
|
||||
onSelect={() => tlstate.style({ size: sizeStyle as SizeStyle })}
|
||||
>
|
||||
{sizes[sizeStyle as SizeStyle]}
|
||||
</ToolButton>
|
||||
))}
|
||||
</DMContent>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
})
|
58
packages/tldraw/src/components/TopPanel/TopPanel.tsx
Normal file
58
packages/tldraw/src/components/TopPanel/TopPanel.tsx
Normal file
|
@ -0,0 +1,58 @@
|
|||
import * as React from 'react'
|
||||
import { Menu } from './Menu'
|
||||
import styled from '~styles'
|
||||
import { PageMenu } from './PageMenu'
|
||||
import { ZoomMenu } from './ZoomMenu'
|
||||
import { DashMenu } from './DashMenu'
|
||||
import { SizeMenu } from './SizeMenu'
|
||||
import { FillCheckbox } from './FillCheckbox'
|
||||
import { ColorMenu } from './ColorMenu'
|
||||
import { Panel } from '~components/Panel'
|
||||
|
||||
interface TopPanelProps {
|
||||
showPages: boolean
|
||||
showMenu: boolean
|
||||
showStyles: boolean
|
||||
showZoom: boolean
|
||||
}
|
||||
|
||||
export function TopPanel({ showPages, showMenu, showStyles, showZoom }: TopPanelProps) {
|
||||
return (
|
||||
<StyledTopPanel>
|
||||
{(showMenu || showPages) && (
|
||||
<Panel side="left">
|
||||
{showMenu && <Menu />}
|
||||
{showPages && <PageMenu />}
|
||||
</Panel>
|
||||
)}
|
||||
<StyledSpacer />
|
||||
{(showStyles || showZoom) && (
|
||||
<Panel side="right">
|
||||
{showStyles && (
|
||||
<>
|
||||
<ColorMenu />
|
||||
<SizeMenu />
|
||||
<DashMenu />
|
||||
<FillCheckbox />
|
||||
</>
|
||||
)}
|
||||
{showZoom && <ZoomMenu />}
|
||||
</Panel>
|
||||
)}
|
||||
</StyledTopPanel>
|
||||
)
|
||||
}
|
||||
|
||||
const StyledTopPanel = styled('div', {
|
||||
width: '100%',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
})
|
||||
|
||||
const StyledSpacer = styled('div', {
|
||||
flexGrow: 2,
|
||||
})
|
43
packages/tldraw/src/components/TopPanel/ZoomMenu.tsx
Normal file
43
packages/tldraw/src/components/TopPanel/ZoomMenu.tsx
Normal file
|
@ -0,0 +1,43 @@
|
|||
import * as React from 'react'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import type { Data } from '~types'
|
||||
import styled from '~styles'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import { DMItem, DMContent } from '~components/DropdownMenu'
|
||||
import { ToolButton } from '~components/ToolButton'
|
||||
|
||||
const zoomSelector = (s: Data) => s.document.pageStates[s.appState.currentPageId].camera.zoom
|
||||
|
||||
export function ZoomMenu() {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const zoom = useSelector(zoomSelector)
|
||||
|
||||
return (
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<FixedWidthToolButton variant="text">{Math.round(zoom * 100)}%</FixedWidthToolButton>
|
||||
</DropdownMenu.Trigger>
|
||||
<DMContent align="end">
|
||||
<DMItem onSelect={tlstate.zoomIn} kbd="#+">
|
||||
Zoom In
|
||||
</DMItem>
|
||||
<DMItem onSelect={tlstate.zoomOut} kbd="#−">
|
||||
Zoom Out
|
||||
</DMItem>
|
||||
<DMItem onSelect={tlstate.zoomToActual} kbd="⇧0">
|
||||
To 100%
|
||||
</DMItem>
|
||||
<DMItem onSelect={tlstate.zoomToFit} kbd="⇧1">
|
||||
To Fit
|
||||
</DMItem>
|
||||
<DMItem onSelect={tlstate.zoomToSelection} kbd="⇧2">
|
||||
To Selection
|
||||
</DMItem>
|
||||
</DMContent>
|
||||
</DropdownMenu.Root>
|
||||
)
|
||||
}
|
||||
|
||||
const FixedWidthToolButton = styled(ToolButton, {
|
||||
minWidth: 56,
|
||||
})
|
1
packages/tldraw/src/components/TopPanel/index.ts
Normal file
1
packages/tldraw/src/components/TopPanel/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from './TopPanel'
|
|
@ -1,381 +0,0 @@
|
|||
import * as React from 'react'
|
||||
import css from '~styles'
|
||||
import * as RadixContextMenu from '@radix-ui/react-context-menu'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { Data, AlignType, DistributeType, StretchType } from '~types'
|
||||
import {
|
||||
Kbd,
|
||||
iconWrapper,
|
||||
breakpoints,
|
||||
rowButton,
|
||||
ContextMenuArrow,
|
||||
ContextMenuDivider,
|
||||
ContextMenuButton,
|
||||
ContextMenuSubMenu,
|
||||
ContextMenuIconButton,
|
||||
ContextMenuRoot,
|
||||
menuContent,
|
||||
} from '../shared'
|
||||
import {
|
||||
ChevronRightIcon,
|
||||
AlignBottomIcon,
|
||||
AlignCenterHorizontallyIcon,
|
||||
AlignCenterVerticallyIcon,
|
||||
AlignLeftIcon,
|
||||
AlignRightIcon,
|
||||
AlignTopIcon,
|
||||
SpaceEvenlyHorizontallyIcon,
|
||||
SpaceEvenlyVerticallyIcon,
|
||||
StretchHorizontallyIcon,
|
||||
StretchVerticallyIcon,
|
||||
} from '@radix-ui/react-icons'
|
||||
|
||||
const has1SelectedIdsSelector = (s: Data) => {
|
||||
return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 0
|
||||
}
|
||||
const has2SelectedIdsSelector = (s: Data) => {
|
||||
return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 1
|
||||
}
|
||||
const has3SelectedIdsSelector = (s: Data) => {
|
||||
return s.document.pageStates[s.appState.currentPageId].selectedIds.length > 2
|
||||
}
|
||||
|
||||
const isDebugModeSelector = (s: Data) => {
|
||||
return s.settings.isDebugMode
|
||||
}
|
||||
|
||||
const hasGroupSelectedSelector = (s: Data) => {
|
||||
return s.document.pageStates[s.appState.currentPageId].selectedIds.some(
|
||||
(id) => s.document.pages[s.appState.currentPageId].shapes[id].children !== undefined
|
||||
)
|
||||
}
|
||||
|
||||
interface ContextMenuProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export const ContextMenu = ({ children }: ContextMenuProps): JSX.Element => {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const hasSelection = useSelector(has1SelectedIdsSelector)
|
||||
const hasTwoOrMore = useSelector(has2SelectedIdsSelector)
|
||||
const hasThreeOrMore = useSelector(has3SelectedIdsSelector)
|
||||
const isDebugMode = useSelector(isDebugModeSelector)
|
||||
const hasGroupSelected = useSelector(hasGroupSelectedSelector)
|
||||
|
||||
const rContent = React.useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleFlipHorizontal = React.useCallback(() => {
|
||||
tlstate.flipHorizontal()
|
||||
}, [tlstate])
|
||||
|
||||
const handleFlipVertical = React.useCallback(() => {
|
||||
tlstate.flipVertical()
|
||||
}, [tlstate])
|
||||
|
||||
const handleDuplicate = React.useCallback(() => {
|
||||
tlstate.duplicate()
|
||||
}, [tlstate])
|
||||
|
||||
const handleGroup = React.useCallback(() => {
|
||||
tlstate.group()
|
||||
}, [tlstate])
|
||||
|
||||
const handleMoveToBack = React.useCallback(() => {
|
||||
tlstate.moveToBack()
|
||||
}, [tlstate])
|
||||
|
||||
const handleMoveBackward = React.useCallback(() => {
|
||||
tlstate.moveBackward()
|
||||
}, [tlstate])
|
||||
|
||||
const handleMoveForward = React.useCallback(() => {
|
||||
tlstate.moveForward()
|
||||
}, [tlstate])
|
||||
|
||||
const handleMoveToFront = React.useCallback(() => {
|
||||
tlstate.moveToFront()
|
||||
}, [tlstate])
|
||||
|
||||
const handleDelete = React.useCallback(() => {
|
||||
tlstate.delete()
|
||||
}, [tlstate])
|
||||
|
||||
const handleCopyJson = React.useCallback(() => {
|
||||
tlstate.copyJson()
|
||||
}, [tlstate])
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
tlstate.copy()
|
||||
}, [tlstate])
|
||||
|
||||
const handlePaste = React.useCallback(() => {
|
||||
tlstate.paste()
|
||||
}, [tlstate])
|
||||
|
||||
const handleCopySvg = React.useCallback(() => {
|
||||
tlstate.copySvg()
|
||||
}, [tlstate])
|
||||
|
||||
const handleUndo = React.useCallback(() => {
|
||||
tlstate.undo()
|
||||
}, [tlstate])
|
||||
|
||||
const handleRedo = React.useCallback(() => {
|
||||
tlstate.redo()
|
||||
}, [tlstate])
|
||||
|
||||
return (
|
||||
<ContextMenuRoot>
|
||||
<RadixContextMenu.Trigger dir="ltr">{children}</RadixContextMenu.Trigger>
|
||||
<RadixContextMenu.Content dir="ltr" className={menuContent()} ref={rContent}>
|
||||
{hasSelection ? (
|
||||
<>
|
||||
<ContextMenuButton onSelect={handleFlipHorizontal}>
|
||||
<span>Flip Horizontal</span>
|
||||
<Kbd variant="menu">⇧H</Kbd>
|
||||
</ContextMenuButton>
|
||||
<ContextMenuButton onSelect={handleFlipVertical}>
|
||||
<span>Flip Vertical</span>
|
||||
<Kbd variant="menu">⇧V</Kbd>
|
||||
</ContextMenuButton>
|
||||
<ContextMenuButton onSelect={handleDuplicate}>
|
||||
<span>Duplicate</span>
|
||||
<Kbd variant="menu">#D</Kbd>
|
||||
</ContextMenuButton>
|
||||
<ContextMenuDivider />
|
||||
{hasGroupSelected ||
|
||||
(hasTwoOrMore && (
|
||||
<>
|
||||
{hasGroupSelected && (
|
||||
<ContextMenuButton onSelect={handleGroup}>
|
||||
<span>Ungroup</span>
|
||||
<Kbd variant="menu">#⇧G</Kbd>
|
||||
</ContextMenuButton>
|
||||
)}
|
||||
{hasTwoOrMore && (
|
||||
<ContextMenuButton onSelect={handleGroup}>
|
||||
<span>Group</span>
|
||||
<Kbd variant="menu">#G</Kbd>
|
||||
</ContextMenuButton>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
<ContextMenuSubMenu label="Move">
|
||||
<ContextMenuButton onSelect={handleMoveToFront}>
|
||||
<span>To Front</span>
|
||||
<Kbd variant="menu">⇧]</Kbd>
|
||||
</ContextMenuButton>
|
||||
<ContextMenuButton onSelect={handleMoveForward}>
|
||||
<span>Forward</span>
|
||||
<Kbd variant="menu">]</Kbd>
|
||||
</ContextMenuButton>
|
||||
<ContextMenuButton onSelect={handleMoveBackward}>
|
||||
<span>Backward</span>
|
||||
<Kbd variant="menu">[</Kbd>
|
||||
</ContextMenuButton>
|
||||
<ContextMenuButton onSelect={handleMoveToBack}>
|
||||
<span>To Back</span>
|
||||
<Kbd variant="menu">⇧[</Kbd>
|
||||
</ContextMenuButton>
|
||||
</ContextMenuSubMenu>
|
||||
<MoveToPageMenu />
|
||||
{hasTwoOrMore && (
|
||||
<AlignDistributeSubMenu hasTwoOrMore={hasTwoOrMore} hasThreeOrMore={hasThreeOrMore} />
|
||||
)}
|
||||
<ContextMenuDivider />
|
||||
<ContextMenuButton onSelect={handleCopy}>
|
||||
<span>Copy</span>
|
||||
<Kbd variant="menu">#C</Kbd>
|
||||
</ContextMenuButton>
|
||||
<ContextMenuButton onSelect={handleCopySvg}>
|
||||
<span>Copy to SVG</span>
|
||||
<Kbd variant="menu">⇧#C</Kbd>
|
||||
</ContextMenuButton>
|
||||
{isDebugMode && (
|
||||
<ContextMenuButton onSelect={handleCopyJson}>
|
||||
<span>Copy to JSON</span>
|
||||
</ContextMenuButton>
|
||||
)}
|
||||
<ContextMenuButton onSelect={handlePaste}>
|
||||
<span>Paste</span>
|
||||
<Kbd variant="menu">#V</Kbd>
|
||||
</ContextMenuButton>
|
||||
<ContextMenuDivider />
|
||||
<ContextMenuButton onSelect={handleDelete}>
|
||||
<span>Delete</span>
|
||||
<Kbd variant="menu">⌫</Kbd>
|
||||
</ContextMenuButton>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ContextMenuButton onSelect={handlePaste}>
|
||||
<span>Paste</span>
|
||||
<Kbd variant="menu">#V</Kbd>
|
||||
</ContextMenuButton>
|
||||
<ContextMenuButton onSelect={handleUndo}>
|
||||
<span>Undo</span>
|
||||
<Kbd variant="menu">#Z</Kbd>
|
||||
</ContextMenuButton>
|
||||
<ContextMenuButton onSelect={handleRedo}>
|
||||
<span>Redo</span>
|
||||
<Kbd variant="menu">#⇧Z</Kbd>
|
||||
</ContextMenuButton>
|
||||
</>
|
||||
)}
|
||||
</RadixContextMenu.Content>
|
||||
</ContextMenuRoot>
|
||||
)
|
||||
}
|
||||
|
||||
function AlignDistributeSubMenu({
|
||||
hasThreeOrMore,
|
||||
}: {
|
||||
hasTwoOrMore: boolean
|
||||
hasThreeOrMore: boolean
|
||||
}) {
|
||||
const { tlstate } = useTLDrawContext()
|
||||
|
||||
const alignTop = React.useCallback(() => {
|
||||
tlstate.align(AlignType.Top)
|
||||
}, [tlstate])
|
||||
|
||||
const alignCenterVertical = React.useCallback(() => {
|
||||
tlstate.align(AlignType.CenterVertical)
|
||||
}, [tlstate])
|
||||
|
||||
const alignBottom = React.useCallback(() => {
|
||||
tlstate.align(AlignType.Bottom)
|
||||
}, [tlstate])
|
||||
|
||||
const stretchVertically = React.useCallback(() => {
|
||||
tlstate.stretch(StretchType.Vertical)
|
||||
}, [tlstate])
|
||||
|
||||
const distributeVertically = React.useCallback(() => {
|
||||
tlstate.distribute(DistributeType.Vertical)
|
||||
}, [tlstate])
|
||||
|
||||
const alignLeft = React.useCallback(() => {
|
||||
tlstate.align(AlignType.Left)
|
||||
}, [tlstate])
|
||||
|
||||
const alignCenterHorizontal = React.useCallback(() => {
|
||||
tlstate.align(AlignType.CenterHorizontal)
|
||||
}, [tlstate])
|
||||
|
||||
const alignRight = React.useCallback(() => {
|
||||
tlstate.align(AlignType.Right)
|
||||
}, [tlstate])
|
||||
|
||||
const stretchHorizontally = React.useCallback(() => {
|
||||
tlstate.stretch(StretchType.Horizontal)
|
||||
}, [tlstate])
|
||||
|
||||
const distributeHorizontally = React.useCallback(() => {
|
||||
tlstate.distribute(DistributeType.Horizontal)
|
||||
}, [tlstate])
|
||||
|
||||
return (
|
||||
<ContextMenuRoot>
|
||||
<RadixContextMenu.TriggerItem className={rowButton({ bp: breakpoints })}>
|
||||
<span>Align / Distribute</span>
|
||||
<div className={iconWrapper({ size: 'small' })}>
|
||||
<ChevronRightIcon />
|
||||
</div>
|
||||
</RadixContextMenu.TriggerItem>
|
||||
<RadixContextMenu.Content
|
||||
className={grid({ selectedStyle: hasThreeOrMore ? 'threeOrMore' : 'twoOrMore' })}
|
||||
sideOffset={2}
|
||||
alignOffset={-2}
|
||||
>
|
||||
<ContextMenuIconButton onSelect={alignLeft}>
|
||||
<AlignLeftIcon />
|
||||
</ContextMenuIconButton>
|
||||
<ContextMenuIconButton onSelect={alignCenterHorizontal}>
|
||||
<AlignCenterHorizontallyIcon />
|
||||
</ContextMenuIconButton>
|
||||
<ContextMenuIconButton onSelect={alignRight}>
|
||||
<AlignRightIcon />
|
||||
</ContextMenuIconButton>
|
||||
<ContextMenuIconButton onSelect={stretchHorizontally}>
|
||||
<StretchHorizontallyIcon />
|
||||
</ContextMenuIconButton>
|
||||
{hasThreeOrMore && (
|
||||
<ContextMenuIconButton onSelect={distributeHorizontally}>
|
||||
<SpaceEvenlyHorizontallyIcon />
|
||||
</ContextMenuIconButton>
|
||||
)}
|
||||
<ContextMenuIconButton onSelect={alignTop}>
|
||||
<AlignTopIcon />
|
||||
</ContextMenuIconButton>
|
||||
<ContextMenuIconButton onSelect={alignCenterVertical}>
|
||||
<AlignCenterVerticallyIcon />
|
||||
</ContextMenuIconButton>
|
||||
<ContextMenuIconButton onSelect={alignBottom}>
|
||||
<AlignBottomIcon />
|
||||
</ContextMenuIconButton>
|
||||
<ContextMenuIconButton onSelect={stretchVertically}>
|
||||
<StretchVerticallyIcon />
|
||||
</ContextMenuIconButton>
|
||||
{hasThreeOrMore && (
|
||||
<ContextMenuIconButton onSelect={distributeVertically}>
|
||||
<SpaceEvenlyVerticallyIcon />
|
||||
</ContextMenuIconButton>
|
||||
)}
|
||||
<ContextMenuArrow offset={13} />
|
||||
</RadixContextMenu.Content>
|
||||
</ContextMenuRoot>
|
||||
)
|
||||
}
|
||||
|
||||
const grid = css(menuContent, {
|
||||
display: 'grid',
|
||||
variants: {
|
||||
selectedStyle: {
|
||||
threeOrMore: {
|
||||
gridTemplateColumns: 'repeat(5, auto)',
|
||||
},
|
||||
twoOrMore: {
|
||||
gridTemplateColumns: 'repeat(4, auto)',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const currentPageIdSelector = (s: Data) => s.appState.currentPageId
|
||||
const documentPagesSelector = (s: Data) => s.document.pages
|
||||
|
||||
function MoveToPageMenu(): JSX.Element | null {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
const currentPageId = useSelector(currentPageIdSelector)
|
||||
const documentPages = useSelector(documentPagesSelector)
|
||||
|
||||
const sorted = Object.values(documentPages)
|
||||
.sort((a, b) => (a.childIndex || 0) - (b.childIndex || 0))
|
||||
.filter((a) => a.id !== currentPageId)
|
||||
|
||||
if (sorted.length === 0) return null
|
||||
|
||||
return (
|
||||
<ContextMenuRoot>
|
||||
<RadixContextMenu.TriggerItem className={rowButton({ bp: breakpoints })}>
|
||||
<span>Move To Page</span>
|
||||
<div className={iconWrapper({ size: 'small' })}>
|
||||
<ChevronRightIcon />
|
||||
</div>
|
||||
</RadixContextMenu.TriggerItem>
|
||||
<RadixContextMenu.Content className={menuContent()} sideOffset={2} alignOffset={-2}>
|
||||
{sorted.map(({ id, name }, i) => (
|
||||
<ContextMenuButton
|
||||
key={id}
|
||||
disabled={id === currentPageId}
|
||||
onSelect={() => tlstate.moveToPage(id)}
|
||||
>
|
||||
<span>{name || `Page ${i}`}</span>
|
||||
</ContextMenuButton>
|
||||
))}
|
||||
<ContextMenuArrow offset={13} />
|
||||
</RadixContextMenu.Content>
|
||||
</ContextMenuRoot>
|
||||
)
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from './context-menu'
|
22
packages/tldraw/src/components/icons/BoxIcon.tsx
Normal file
22
packages/tldraw/src/components/icons/BoxIcon.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import * as React from 'react'
|
||||
|
||||
export function BoxIcon({
|
||||
fill = 'none',
|
||||
stroke = 'currentColor',
|
||||
}: {
|
||||
fill?: string
|
||||
stroke?: string
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
stroke={stroke}
|
||||
fill={fill}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect x="4" y="4" width="16" height="16" rx="2" strokeWidth="2" />
|
||||
</svg>
|
||||
)
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
|
||||
function SvgCheck(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
||||
function CheckIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
||||
return (
|
||||
<svg viewBox="0 0 15 15" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
|
@ -17,4 +17,4 @@ function SvgCheck(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
|||
)
|
||||
}
|
||||
|
||||
export default SvgCheck
|
||||
export default CheckIcon
|
17
packages/tldraw/src/components/icons/DashDashedIcon.tsx
Normal file
17
packages/tldraw/src/components/icons/DashDashedIcon.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
import * as React from 'react'
|
||||
|
||||
export function DashDashedIcon(): JSX.Element {
|
||||
return (
|
||||
<svg width="24" height="24" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle
|
||||
cx={12}
|
||||
cy={12}
|
||||
r={8}
|
||||
fill="none"
|
||||
strokeWidth={2.5}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={50.26548 * 0.1}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
19
packages/tldraw/src/components/icons/DashDottedIcon.tsx
Normal file
19
packages/tldraw/src/components/icons/DashDottedIcon.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import * as React from 'react'
|
||||
|
||||
const dottedDasharray = `${50.26548 * 0.025} ${50.26548 * 0.1}`
|
||||
|
||||
export function DashDottedIcon(): JSX.Element {
|
||||
return (
|
||||
<svg width="24" height="24" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle
|
||||
cx={12}
|
||||
cy={12}
|
||||
r={8}
|
||||
fill="none"
|
||||
strokeWidth={2.5}
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={dottedDasharray}
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
19
packages/tldraw/src/components/icons/DashDrawIcon.tsx
Normal file
19
packages/tldraw/src/components/icons/DashDrawIcon.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import * as React from 'react'
|
||||
|
||||
export function DashDrawIcon(): JSX.Element {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="1 1.5 21 22"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M10.0162 19.2768C10.0162 19.2768 9.90679 19.2517 9.6879 19.2017C9.46275 19.1454 9.12816 19.0422 8.68413 18.8921C8.23384 18.7358 7.81482 18.545 7.42707 18.3199C7.03307 18.101 6.62343 17.7883 6.19816 17.3818C5.77289 16.9753 5.33511 16.3718 4.88482 15.5713C4.43453 14.7645 4.1531 13.8545 4.04053 12.8414C3.92795 11.822 4.04991 10.8464 4.40639 9.91451C4.76286 8.98266 5.39452 8.10084 6.30135 7.26906C7.21444 6.44353 8.29325 5.83377 9.5378 5.43976C10.7823 5.05202 11.833 4.92068 12.6898 5.04576C13.5466 5.16459 14.3878 5.43664 15.2133 5.86191C16.0388 6.28718 16.7768 6.8688 17.4272 7.60678C18.0714 8.34475 18.5404 9.21406 18.8344 10.2147C19.1283 11.2153 19.1721 12.2598 18.9657 13.348C18.7593 14.4299 18.2872 15.4337 17.5492 16.3593C16.8112 17.2849 15.9263 18.0072 14.8944 18.5263C13.8624 19.0391 12.9056 19.3174 12.0238 19.3612C11.142 19.405 10.2101 19.2705 9.22823 18.9578C8.24635 18.6451 7.35828 18.151 6.56402 17.4756C5.77601 16.8002 6.08871 16.8658 7.50212 17.6726C8.90927 18.4731 10.1444 18.8484 11.2076 18.7983C12.2645 18.7545 13.2965 18.4825 14.3034 17.9822C15.3102 17.4819 16.1264 16.8221 16.7518 16.0028C17.3772 15.1835 17.7681 14.3111 17.9244 13.3855C18.0808 12.4599 18.0401 11.5781 17.8025 10.74C17.5586 9.902 17.1739 9.15464 16.6486 8.49797C16.1233 7.8413 15.2289 7.27844 13.9656 6.80939C12.7086 6.34034 11.4203 6.20901 10.1007 6.41539C8.78732 6.61552 7.69599 7.06893 6.82669 7.77564C5.96363 8.48859 5.34761 9.26409 4.97863 10.1021C4.60964 10.9402 4.45329 11.8376 4.50958 12.7945C4.56586 13.7513 4.79101 14.6238 5.18501 15.4118C5.57276 16.1998 5.96363 16.8002 6.35764 17.2129C6.75164 17.6257 7.13313 17.9509 7.50212 18.1886C7.87736 18.4325 8.28074 18.642 8.71227 18.8171C9.15005 18.9922 9.47839 19.111 9.69728 19.1736C9.91617 19.2361 10.0256 19.2705 10.0256 19.2768H10.0162Z"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
9
packages/tldraw/src/components/icons/DashSolidIcon.tsx
Normal file
9
packages/tldraw/src/components/icons/DashSolidIcon.tsx
Normal file
|
@ -0,0 +1,9 @@
|
|||
import * as React from 'react'
|
||||
|
||||
export function DashSolidIcon(): JSX.Element {
|
||||
return (
|
||||
<svg width="24" height="24" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx={12} cy={12} r={8} fill="none" strokeWidth={2} strokeLinecap="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
18
packages/tldraw/src/components/icons/IsFilledIcon.tsx
Normal file
18
packages/tldraw/src/components/icons/IsFilledIcon.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import * as React from 'react'
|
||||
|
||||
export function IsFilledIcon(): JSX.Element {
|
||||
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=".3"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import * as React from 'react'
|
||||
|
||||
function SvgRedo(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
||||
export function RedoIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
||||
return (
|
||||
<svg viewBox="0 0 15 15" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<svg viewBox="0 -1 15 15" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
|
@ -16,5 +16,3 @@ function SvgRedo(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
|||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default SvgRedo
|
12
packages/tldraw/src/components/icons/SizeLargeIcon.tsx
Normal file
12
packages/tldraw/src/components/icons/SizeLargeIcon.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import * as React from 'react'
|
||||
|
||||
export function SizeLargeIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
||||
return (
|
||||
<svg viewBox="-4 -4 32 32" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M7.68191 19C7.53525 19 7.46191 18.9267 7.46191 18.78V5H10.1219C10.2686 5 10.3419 5.07333 10.3419 5.22V16.56H13.4419V15.02H15.7619C15.9086 15.02 15.9819 15.0933 15.9819 15.24V19H7.68191Z"
|
||||
fill="black"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
12
packages/tldraw/src/components/icons/SizeMediumIcon.tsx
Normal file
12
packages/tldraw/src/components/icons/SizeMediumIcon.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import * as React from 'react'
|
||||
|
||||
export function SizeMediumIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
||||
return (
|
||||
<svg viewBox="-4 -4 32 32" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M8.16191 19H5.68191C5.53525 19 5.46191 18.9267 5.46191 18.78V5H8.76191C8.88191 5 8.97525 5.03333 9.04191 5.1C9.10858 5.15333 9.17525 5.27333 9.24191 5.46C9.72191 6.59333 10.1686 7.7 10.5819 8.78C11.0086 9.84667 11.4352 10.98 11.8619 12.18H12.1619C12.6019 10.9667 13.0352 9.79333 13.4619 8.66C13.8886 7.52667 14.3552 6.30667 14.8619 5H18.3219C18.4686 5 18.5419 5.07333 18.5419 5.22V19H16.0619C15.9152 19 15.8419 18.9267 15.8419 18.78V16.26C15.8419 15.5267 15.8486 14.8133 15.8619 14.12C15.8886 13.4267 15.9286 12.6867 15.9819 11.9C16.0486 11.1 16.1419 10.1933 16.2619 9.18H15.9019C15.4352 10.3533 14.9486 11.5667 14.4419 12.82C13.9486 14.06 13.4819 15.2333 13.0419 16.34H11.1019C11.0619 16.34 11.0152 16.3333 10.9619 16.32C10.9219 16.2933 10.8886 16.2467 10.8619 16.18C10.4619 15.18 10.0086 14.06 9.50191 12.82C9.00858 11.58 8.53525 10.3667 8.08191 9.18H7.70191C7.83525 10.18 7.93525 11.0733 8.00191 11.86C8.06858 12.6467 8.10858 13.3933 8.12191 14.1C8.14858 14.8067 8.16191 15.5267 8.16191 16.26V19Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
12
packages/tldraw/src/components/icons/SizeSmallIcon.tsx
Normal file
12
packages/tldraw/src/components/icons/SizeSmallIcon.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
import * as React from 'react'
|
||||
|
||||
export function SizeSmallIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
||||
return (
|
||||
<svg viewBox="-4 -4 32 32" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
d="M12.4239 4.62C13.3572 4.62 14.1572 4.73333 14.8239 4.96C15.4906 5.17333 15.9772 5.43333 16.2839 5.74C16.3639 5.82 16.4039 5.94 16.4039 6.1V8.86H14.0639C13.9172 8.86 13.8439 8.78666 13.8439 8.64V7.26C13.4306 7.12666 12.9572 7.06 12.4239 7.06C11.6506 7.06 11.0639 7.18 10.6639 7.42C10.2639 7.66 10.0639 8.04666 10.0639 8.58V9C10.0639 9.38666 10.1639 9.69333 10.3639 9.92C10.5772 10.1333 11.0306 10.3467 11.7239 10.56L13.6439 11.14C14.4706 11.38 15.1172 11.66 15.5839 11.98C16.0506 12.3 16.3772 12.68 16.5639 13.12C16.7639 13.5467 16.8639 14.0733 16.8639 14.7V15.62C16.8639 16.7933 16.4039 17.7133 15.4839 18.38C14.5639 19.0467 13.2839 19.38 11.6439 19.38C10.6706 19.38 9.79723 19.2867 9.0239 19.1C8.2639 18.9133 7.71056 18.6533 7.3639 18.32C7.3239 18.28 7.29056 18.24 7.2639 18.2C7.25056 18.1467 7.2439 18.06 7.2439 17.94V15.74H7.6239C8.2239 16.1533 8.85056 16.4533 9.5039 16.64C10.1572 16.8267 10.9306 16.92 11.8239 16.92C12.6506 16.92 13.2506 16.7867 13.6239 16.52C14.0106 16.2533 14.2039 15.9333 14.2039 15.56V14.88C14.2039 14.6667 14.1639 14.48 14.0839 14.32C14.0172 14.16 13.8706 14.0133 13.6439 13.88C13.4172 13.7467 13.0572 13.6067 12.5639 13.46L10.6639 12.88C9.7839 12.6133 9.11056 12.3 8.6439 11.94C8.17723 11.58 7.85056 11.18 7.6639 10.74C7.49056 10.3 7.4039 9.83333 7.4039 9.34V8.38C7.4039 7.64666 7.61056 7 8.0239 6.44C8.43723 5.88 9.01723 5.44 9.7639 5.12C10.5239 4.78666 11.4106 4.62 12.4239 4.62Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from 'react'
|
||||
|
||||
function SvgTrash(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
||||
export function TrashIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
||||
return (
|
||||
<svg viewBox="0 0 15 15" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
|
@ -21,5 +21,3 @@ function SvgTrash(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
|||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default SvgTrash
|
|
@ -1,8 +1,8 @@
|
|||
import * as React from 'react'
|
||||
|
||||
function SvgUndo(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
||||
export function UndoIcon(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
||||
return (
|
||||
<svg viewBox="0 0 15 15" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<svg viewBox="0 -1 15 15" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {...props}>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
|
@ -16,5 +16,3 @@ function SvgUndo(props: React.SVGProps<SVGSVGElement>): JSX.Element {
|
|||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default SvgUndo
|
14
packages/tldraw/src/components/icons/index.ts
Normal file
14
packages/tldraw/src/components/icons/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
export * from './BoxIcon'
|
||||
export * from './CheckIcon'
|
||||
export * from './CircleIcon'
|
||||
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'
|
|
@ -1,5 +0,0 @@
|
|||
export { default as Redo } from './redo'
|
||||
export { default as Trash } from './trash'
|
||||
export { default as Undo } from './undo'
|
||||
export { default as Check } from './check'
|
||||
export { default as CircleIcon } from './circle'
|
|
@ -1,5 +0,0 @@
|
|||
export * from './shared/tooltip'
|
||||
export * from './shared/kbd'
|
||||
export * from './shared'
|
||||
export * from './icons'
|
||||
export * from './tldraw'
|
|
@ -1 +0,0 @@
|
|||
export * from './menu'
|
|
@ -1,9 +0,0 @@
|
|||
import * as React from 'react'
|
||||
import { Menu } from './menu'
|
||||
import { renderWithContext } from '~test'
|
||||
|
||||
describe('menu', () => {
|
||||
test('mounts component without crashing', () => {
|
||||
renderWithContext(<Menu />)
|
||||
})
|
||||
})
|
|
@ -1,95 +0,0 @@
|
|||
import * as React from 'react'
|
||||
import { ExitIcon, HamburgerMenuIcon } from '@radix-ui/react-icons'
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import {
|
||||
floatingContainer,
|
||||
DropdownMenuRoot,
|
||||
menuContent,
|
||||
iconButton,
|
||||
breakpoints,
|
||||
DropdownMenuButton,
|
||||
DropdownMenuSubMenu,
|
||||
DropdownMenuDivider,
|
||||
iconWrapper,
|
||||
Kbd,
|
||||
} from '~components/shared'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import { Preferences } from './preferences'
|
||||
|
||||
export const Menu = React.memo(() => {
|
||||
const { tlstate } = useTLDrawContext()
|
||||
|
||||
const handleNew = React.useCallback(() => {
|
||||
if (window.confirm('Are you sure you want to start a new project?')) {
|
||||
tlstate.newProject()
|
||||
}
|
||||
}, [tlstate])
|
||||
|
||||
const handleSave = React.useCallback(() => {
|
||||
tlstate.saveProject()
|
||||
}, [tlstate])
|
||||
|
||||
const handleLoad = React.useCallback(() => {
|
||||
tlstate.loadProject()
|
||||
}, [tlstate])
|
||||
|
||||
const handleSignOut = React.useCallback(() => {
|
||||
tlstate.signOut()
|
||||
}, [tlstate])
|
||||
|
||||
return (
|
||||
<div className={floatingContainer()}>
|
||||
<DropdownMenuRoot>
|
||||
<DropdownMenu.Trigger dir="ltr" className={iconButton({ bp: breakpoints })}>
|
||||
<HamburgerMenuIcon />
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content dir="ltr" className={menuContent()} sideOffset={8} align="end">
|
||||
<DropdownMenuButton onSelect={handleNew}>
|
||||
<span>New Project</span>
|
||||
<Kbd variant="menu">#N</Kbd>
|
||||
</DropdownMenuButton>
|
||||
<DropdownMenuDivider dir="ltr" />
|
||||
<DropdownMenuButton disabled onSelect={handleLoad}>
|
||||
<span>Open...</span>
|
||||
<Kbd variant="menu">#L</Kbd>
|
||||
</DropdownMenuButton>
|
||||
<RecentFiles />
|
||||
<DropdownMenuDivider dir="ltr" />
|
||||
<DropdownMenuButton disabled onSelect={handleSave}>
|
||||
<span>Save</span>
|
||||
<Kbd variant="menu">#S</Kbd>
|
||||
</DropdownMenuButton>
|
||||
<DropdownMenuButton disabled onSelect={handleSave}>
|
||||
<span>Save As...</span>
|
||||
<Kbd variant="menu">⇧#S</Kbd>
|
||||
</DropdownMenuButton>
|
||||
<DropdownMenuDivider dir="ltr" />
|
||||
<Preferences />
|
||||
<DropdownMenuDivider dir="ltr" />
|
||||
<DropdownMenuButton disabled onSelect={handleSignOut}>
|
||||
<span>Sign Out</span>
|
||||
<div className={iconWrapper({ size: 'small' })}>
|
||||
<ExitIcon />
|
||||
</div>
|
||||
</DropdownMenuButton>
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenuRoot>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
function RecentFiles() {
|
||||
return (
|
||||
<DropdownMenuSubMenu label="Open Recent..." disabled={true}>
|
||||
<DropdownMenuButton>
|
||||
<span>Project A</span>
|
||||
</DropdownMenuButton>
|
||||
<DropdownMenuButton>
|
||||
<span>Project B</span>
|
||||
</DropdownMenuButton>
|
||||
<DropdownMenuButton>
|
||||
<span>Project C</span>
|
||||
</DropdownMenuButton>
|
||||
</DropdownMenuSubMenu>
|
||||
)
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
import * as React from 'react'
|
||||
import {
|
||||
DropdownMenuDivider,
|
||||
DropdownMenuSubMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
Kbd,
|
||||
} from '~components/shared'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
import type { Data } from '~types'
|
||||
|
||||
const settingsSelector = (s: Data) => s.settings
|
||||
|
||||
export function Preferences() {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
|
||||
const settings = useSelector(settingsSelector)
|
||||
|
||||
const toggleDebugMode = React.useCallback(() => {
|
||||
tlstate.setSetting('isDebugMode', (v) => !v)
|
||||
}, [tlstate])
|
||||
|
||||
const toggleDarkMode = React.useCallback(() => {
|
||||
tlstate.setSetting('isDarkMode', (v) => !v)
|
||||
}, [tlstate])
|
||||
|
||||
const toggleFocusMode = React.useCallback(() => {
|
||||
tlstate.setSetting('isFocusMode', (v) => !v)
|
||||
}, [tlstate])
|
||||
|
||||
const toggleRotateHandle = React.useCallback(() => {
|
||||
tlstate.setSetting('showRotateHandles', (v) => !v)
|
||||
}, [tlstate])
|
||||
|
||||
const toggleBoundShapesHandle = React.useCallback(() => {
|
||||
tlstate.setSetting('showBindingHandles', (v) => !v)
|
||||
}, [tlstate])
|
||||
|
||||
const toggleisSnapping = React.useCallback(() => {
|
||||
tlstate.setSetting('isSnapping', (v) => !v)
|
||||
}, [tlstate])
|
||||
|
||||
const toggleCloneControls = React.useCallback(() => {
|
||||
tlstate.setSetting('showCloneHandles', (v) => !v)
|
||||
}, [tlstate])
|
||||
|
||||
return (
|
||||
<DropdownMenuSubMenu label="Preferences">
|
||||
<DropdownMenuCheckboxItem checked={settings.isDarkMode} onCheckedChange={toggleDarkMode}>
|
||||
<span>Dark Mode</span>
|
||||
<Kbd variant="menu">#⇧D</Kbd>
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem checked={settings.isFocusMode} onCheckedChange={toggleFocusMode}>
|
||||
<span>Focus Mode</span>
|
||||
<Kbd variant="menu">⇧.</Kbd>
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem checked={settings.isDebugMode} onCheckedChange={toggleDebugMode}>
|
||||
<span>Debug Mode</span>
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuDivider />
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={settings.showRotateHandles}
|
||||
onCheckedChange={toggleRotateHandle}
|
||||
>
|
||||
<span>Rotate Handles</span>
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={settings.showBindingHandles}
|
||||
onCheckedChange={toggleBoundShapesHandle}
|
||||
>
|
||||
<span>Binding Handles</span>
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
checked={settings.showCloneHandles}
|
||||
onCheckedChange={toggleCloneControls}
|
||||
>
|
||||
<span>Clone Handles</span>
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem checked={settings.isSnapping} onCheckedChange={toggleisSnapping}>
|
||||
<span>Always Show Snaps</span>
|
||||
</DropdownMenuCheckboxItem>
|
||||
</DropdownMenuSubMenu>
|
||||
)
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from './page-options-dialog'
|
|
@ -1,9 +0,0 @@
|
|||
import * as React from 'react'
|
||||
import { PageOptionsDialog } from './page-options-dialog'
|
||||
import { mockDocument, renderWithContext } from '~test'
|
||||
|
||||
describe('page options dialog', () => {
|
||||
test('mounts component without crashing', () => {
|
||||
renderWithContext(<PageOptionsDialog page={mockDocument.pages.page1} />)
|
||||
})
|
||||
})
|
|
@ -1,106 +0,0 @@
|
|||
import * as React from 'react'
|
||||
import * as Dialog from '@radix-ui/react-alert-dialog'
|
||||
import { MixerVerticalIcon } from '@radix-ui/react-icons'
|
||||
import {
|
||||
breakpoints,
|
||||
iconButton,
|
||||
dialogOverlay,
|
||||
dialogContent,
|
||||
rowButton,
|
||||
divider,
|
||||
} from '~components/shared'
|
||||
import type { Data, TLDrawPage } from '~types'
|
||||
import { useTLDrawContext } from '~hooks'
|
||||
|
||||
const canDeleteSelector = (s: Data) => {
|
||||
return Object.keys(s.document.pages).length > 1
|
||||
}
|
||||
|
||||
interface PageOptionsDialogProps {
|
||||
page: TLDrawPage
|
||||
onOpen?: () => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogProps): JSX.Element {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
|
||||
const canDelete = useSelector(canDeleteSelector)
|
||||
|
||||
const rInput = React.useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleDuplicate = React.useCallback(() => {
|
||||
tlstate.duplicatePage(page.id)
|
||||
onClose?.()
|
||||
}, [tlstate])
|
||||
|
||||
const handleDelete = React.useCallback(() => {
|
||||
if (window.confirm(`Are you sure you want to delete this page?`)) {
|
||||
tlstate.deletePage(page.id)
|
||||
onClose?.()
|
||||
}
|
||||
}, [tlstate])
|
||||
|
||||
const handleOpenChange = React.useCallback(
|
||||
(isOpen: boolean) => {
|
||||
setIsOpen(isOpen)
|
||||
|
||||
if (isOpen) {
|
||||
onOpen?.()
|
||||
return
|
||||
}
|
||||
},
|
||||
[tlstate, name]
|
||||
)
|
||||
|
||||
function stopPropagation(e: React.KeyboardEvent<HTMLDivElement>) {
|
||||
e.stopPropagation()
|
||||
}
|
||||
|
||||
// TODO: Replace with text input
|
||||
function handleRename() {
|
||||
const nextName = window.prompt('New name:', page.name)
|
||||
tlstate.renamePage(page.id, nextName || page.name || 'Page')
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isOpen) {
|
||||
requestAnimationFrame(() => {
|
||||
rInput.current?.focus()
|
||||
rInput.current?.select()
|
||||
})
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
return (
|
||||
<Dialog.Root open={isOpen} onOpenChange={handleOpenChange}>
|
||||
<Dialog.Trigger className={iconButton({ bp: breakpoints, size: 'small' })} data-shy="true">
|
||||
<MixerVerticalIcon />
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Overlay className={dialogOverlay()} />
|
||||
<Dialog.Content
|
||||
className={dialogContent()}
|
||||
onKeyDown={stopPropagation}
|
||||
onKeyUp={stopPropagation}
|
||||
>
|
||||
<Dialog.Action className={rowButton({ bp: breakpoints })} onClick={handleRename}>
|
||||
Rename
|
||||
</Dialog.Action>
|
||||
<Dialog.Action className={rowButton({ bp: breakpoints })} onClick={handleDuplicate}>
|
||||
Duplicate
|
||||
</Dialog.Action>
|
||||
<Dialog.Action
|
||||
className={rowButton({ bp: breakpoints, warn: true })}
|
||||
disabled={!canDelete}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
Delete
|
||||
</Dialog.Action>
|
||||
<div className={divider()} />
|
||||
<Dialog.Cancel className={rowButton({ bp: breakpoints })}>Cancel</Dialog.Cancel>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
)
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
export * from './page-panel'
|
|
@ -1,9 +0,0 @@
|
|||
import * as React from 'react'
|
||||
import { PagePanel } from './page-panel'
|
||||
import { renderWithContext } from '~test'
|
||||
|
||||
describe('page panel', () => {
|
||||
test('mounts component without crashing', () => {
|
||||
renderWithContext(<PagePanel />)
|
||||
})
|
||||
})
|
|
@ -1,18 +0,0 @@
|
|||
import css from '~styles'
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Buttons Row */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
export const buttonsRow = css({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
outline: 'none',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
padding: 0,
|
||||
})
|
|
@ -1,166 +0,0 @@
|
|||
import * as React from 'react'
|
||||
import { CheckIcon, ChevronRightIcon } from '@radix-ui/react-icons'
|
||||
import {
|
||||
Root as CMRoot,
|
||||
TriggerItem as CMTriggerItem,
|
||||
Separator as CMSeparator,
|
||||
Item as CMItem,
|
||||
Arrow as CMArrow,
|
||||
Content as CMContent,
|
||||
ItemIndicator as CMItemIndicator,
|
||||
CheckboxItem as CMCheckboxItem,
|
||||
} from '@radix-ui/react-context-menu'
|
||||
import { breakpoints } from './breakpoints'
|
||||
import { rowButton } from './row-button'
|
||||
import { iconButton } from './icon-button'
|
||||
import { iconWrapper } from './icon-wrapper'
|
||||
import { menuContent } from './menu'
|
||||
import css from '~styles'
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Context Menu */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
export interface ContextMenuRootProps {
|
||||
onOpenChange?: (isOpen: boolean) => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function ContextMenuRoot({ onOpenChange, children }: ContextMenuRootProps): JSX.Element {
|
||||
return (
|
||||
<CMRoot dir="ltr" onOpenChange={onOpenChange}>
|
||||
{children}
|
||||
</CMRoot>
|
||||
)
|
||||
}
|
||||
|
||||
export interface ContextMenuSubMenuProps {
|
||||
label: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function ContextMenuSubMenu({ children, label }: ContextMenuSubMenuProps): JSX.Element {
|
||||
return (
|
||||
<CMRoot dir="ltr">
|
||||
<CMTriggerItem dir="ltr" className={rowButton({ bp: breakpoints })}>
|
||||
<span>{label}</span>
|
||||
<div className={iconWrapper({ size: 'small' })}>
|
||||
<ChevronRightIcon />
|
||||
</div>
|
||||
</CMTriggerItem>
|
||||
<CMContent dir="ltr" className={menuContent()} sideOffset={2} alignOffset={-2}>
|
||||
{children}
|
||||
<ContextMenuArrow offset={13} />
|
||||
</CMContent>
|
||||
</CMRoot>
|
||||
)
|
||||
}
|
||||
|
||||
const contextMenuDivider = css({
|
||||
backgroundColor: '$hover',
|
||||
height: 1,
|
||||
margin: '$2 -$2',
|
||||
})
|
||||
|
||||
export const ContextMenuDivider = React.forwardRef<
|
||||
React.ElementRef<typeof CMSeparator>,
|
||||
React.ComponentProps<typeof CMSeparator>
|
||||
>((props, forwardedRef) => (
|
||||
<CMSeparator
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
className={contextMenuDivider({ className: props.className })}
|
||||
/>
|
||||
))
|
||||
|
||||
const contextMenuArrow = css({
|
||||
fill: '$panel',
|
||||
})
|
||||
|
||||
export const ContextMenuArrow = React.forwardRef<
|
||||
React.ElementRef<typeof CMArrow>,
|
||||
React.ComponentProps<typeof CMArrow>
|
||||
>((props, forwardedRef) => (
|
||||
<CMArrow
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
className={contextMenuArrow({ className: props.className })}
|
||||
/>
|
||||
))
|
||||
|
||||
export interface ContextMenuButtonProps {
|
||||
onSelect?: () => void
|
||||
disabled?: boolean
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function ContextMenuButton({
|
||||
onSelect,
|
||||
children,
|
||||
disabled = false,
|
||||
}: ContextMenuButtonProps): JSX.Element {
|
||||
return (
|
||||
<CMItem
|
||||
dir="ltr"
|
||||
className={rowButton({ bp: breakpoints })}
|
||||
disabled={disabled}
|
||||
onSelect={onSelect}
|
||||
>
|
||||
{children}
|
||||
</CMItem>
|
||||
)
|
||||
}
|
||||
|
||||
interface ContextMenuIconButtonProps {
|
||||
onSelect: () => void
|
||||
disabled?: boolean
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function ContextMenuIconButton({
|
||||
onSelect,
|
||||
children,
|
||||
disabled = false,
|
||||
}: ContextMenuIconButtonProps): JSX.Element {
|
||||
return (
|
||||
<CMItem
|
||||
dir="ltr"
|
||||
className={iconButton({ bp: breakpoints })}
|
||||
disabled={disabled}
|
||||
onSelect={onSelect}
|
||||
>
|
||||
{children}
|
||||
</CMItem>
|
||||
)
|
||||
}
|
||||
|
||||
interface ContextMenuCheckboxItemProps {
|
||||
checked: boolean
|
||||
disabled?: boolean
|
||||
onCheckedChange: (isChecked: boolean) => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function ContextMenuCheckboxItem({
|
||||
checked,
|
||||
disabled = false,
|
||||
onCheckedChange,
|
||||
children,
|
||||
}: ContextMenuCheckboxItemProps): JSX.Element {
|
||||
return (
|
||||
<CMCheckboxItem
|
||||
dir="ltr"
|
||||
className={rowButton({ bp: breakpoints })}
|
||||
onCheckedChange={onCheckedChange}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
<CMItemIndicator dir="ltr">
|
||||
<div className={iconWrapper({ size: 'small' })}>
|
||||
<CheckIcon />
|
||||
</div>
|
||||
</CMItemIndicator>
|
||||
</CMCheckboxItem>
|
||||
)
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
import css from '~styles'
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Dialog */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
export const dialogContent = css({
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
minWidth: 240,
|
||||
maxWidth: 'fit-content',
|
||||
maxHeight: '85vh',
|
||||
marginTop: '-5vh',
|
||||
pointerEvents: 'all',
|
||||
backgroundColor: '$panel',
|
||||
border: '1px solid $panel',
|
||||
padding: '$0',
|
||||
boxShadow: '$4',
|
||||
borderRadius: '4px',
|
||||
font: '$ui',
|
||||
|
||||
'&:focus': {
|
||||
outline: 'none',
|
||||
},
|
||||
})
|
||||
|
||||
export const dialogOverlay = css({
|
||||
backgroundColor: 'rgba(0, 0, 0, .15)',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
})
|
||||
|
||||
export const dialogInputWrapper = css({
|
||||
padding: '$4 $2',
|
||||
})
|
||||
|
||||
export const dialogTitleRow = css({
|
||||
display: 'flex',
|
||||
padding: '0 0 0 $4',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
|
||||
h3: {
|
||||
fontSize: '$1',
|
||||
},
|
||||
})
|
|
@ -1,205 +0,0 @@
|
|||
import * as React from 'react'
|
||||
import { CheckIcon, ChevronRightIcon } from '@radix-ui/react-icons'
|
||||
import {
|
||||
Root as DMRoot,
|
||||
TriggerItem as DMTriggerItem,
|
||||
Separator as DMSeparator,
|
||||
Item as DMItem,
|
||||
Arrow as DMArrow,
|
||||
Content as DMContent,
|
||||
Trigger as DMTrigger,
|
||||
ItemIndicator as DMItemIndicator,
|
||||
CheckboxItem as DMCheckboxItem,
|
||||
} from '@radix-ui/react-dropdown-menu'
|
||||
|
||||
import { Tooltip } from './tooltip'
|
||||
import { breakpoints } from './breakpoints'
|
||||
import { rowButton } from './row-button'
|
||||
import { iconButton } from './icon-button'
|
||||
import { iconWrapper } from './icon-wrapper'
|
||||
import { menuContent } from './menu'
|
||||
|
||||
import css from '~styles'
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Dropdown Menu */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
export interface DropdownMenuRootProps {
|
||||
isOpen?: boolean
|
||||
onOpenChange?: (isOpen: boolean) => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function DropdownMenuRoot({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
children,
|
||||
}: DropdownMenuRootProps): JSX.Element {
|
||||
return (
|
||||
<DMRoot dir="ltr" open={isOpen} onOpenChange={onOpenChange}>
|
||||
{children}
|
||||
</DMRoot>
|
||||
)
|
||||
}
|
||||
|
||||
export interface DropdownMenuSubMenuProps {
|
||||
label: string
|
||||
disabled?: boolean
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function DropdownMenuSubMenu({
|
||||
children,
|
||||
disabled = false,
|
||||
label,
|
||||
}: DropdownMenuSubMenuProps): JSX.Element {
|
||||
return (
|
||||
<DMRoot dir="ltr">
|
||||
<DMTriggerItem dir="ltr" className={rowButton({ bp: breakpoints })} disabled={disabled}>
|
||||
<span>{label}</span>
|
||||
<div className={iconWrapper({ size: 'small' })}>
|
||||
<ChevronRightIcon />
|
||||
</div>
|
||||
</DMTriggerItem>
|
||||
<DMContent dir="ltr" className={menuContent()} sideOffset={2} alignOffset={-2}>
|
||||
{children}
|
||||
<DropdownMenuArrow offset={13} />
|
||||
</DMContent>
|
||||
</DMRoot>
|
||||
)
|
||||
}
|
||||
|
||||
export const dropdownMenuDivider = css({
|
||||
backgroundColor: '$hover',
|
||||
height: 1,
|
||||
marginTop: '$2',
|
||||
marginRight: '-$2',
|
||||
marginBottom: '$2',
|
||||
marginLeft: '-$2',
|
||||
})
|
||||
|
||||
export const DropdownMenuDivider = React.forwardRef<
|
||||
React.ElementRef<typeof DMSeparator>,
|
||||
React.ComponentProps<typeof DMSeparator>
|
||||
>((props, forwardedRef) => (
|
||||
<DMSeparator
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
className={dropdownMenuDivider({ className: props.className })}
|
||||
/>
|
||||
))
|
||||
|
||||
export const dropdownMenuArrow = css({
|
||||
fill: '$panel',
|
||||
})
|
||||
|
||||
export const DropdownMenuArrow = React.forwardRef<
|
||||
React.ElementRef<typeof DMArrow>,
|
||||
React.ComponentProps<typeof DMArrow>
|
||||
>((props, forwardedRef) => (
|
||||
<DMArrow
|
||||
{...props}
|
||||
ref={forwardedRef}
|
||||
className={dropdownMenuArrow({ className: props.className })}
|
||||
/>
|
||||
))
|
||||
|
||||
export interface DropdownMenuButtonProps {
|
||||
onSelect?: () => void
|
||||
disabled?: boolean
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function DropdownMenuButton({
|
||||
onSelect,
|
||||
children,
|
||||
disabled = false,
|
||||
}: DropdownMenuButtonProps): JSX.Element {
|
||||
return (
|
||||
<DMItem
|
||||
dir="ltr"
|
||||
className={rowButton({ bp: breakpoints })}
|
||||
disabled={disabled}
|
||||
onSelect={onSelect}
|
||||
>
|
||||
{children}
|
||||
</DMItem>
|
||||
)
|
||||
}
|
||||
|
||||
interface DropdownMenuIconButtonProps {
|
||||
onSelect: () => void
|
||||
disabled?: boolean
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function DropdownMenuIconButton({
|
||||
onSelect,
|
||||
children,
|
||||
disabled = false,
|
||||
}: DropdownMenuIconButtonProps): JSX.Element {
|
||||
return (
|
||||
<DMItem
|
||||
dir="ltr"
|
||||
className={iconButton({ bp: breakpoints })}
|
||||
disabled={disabled}
|
||||
onSelect={onSelect}
|
||||
>
|
||||
{children}
|
||||
</DMItem>
|
||||
)
|
||||
}
|
||||
|
||||
interface DropdownMenuIconTriggerButtonProps {
|
||||
label: string
|
||||
kbd?: string
|
||||
disabled?: boolean
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function DropdownMenuIconTriggerButton({
|
||||
label,
|
||||
kbd,
|
||||
children,
|
||||
disabled = false,
|
||||
}: DropdownMenuIconTriggerButtonProps): JSX.Element {
|
||||
return (
|
||||
<DMTrigger dir="ltr" className={iconButton({ bp: breakpoints })} disabled={disabled}>
|
||||
<Tooltip label={label} kbd={kbd}>
|
||||
{children}
|
||||
</Tooltip>
|
||||
</DMTrigger>
|
||||
)
|
||||
}
|
||||
|
||||
interface MenuCheckboxItemProps {
|
||||
checked: boolean
|
||||
disabled?: boolean
|
||||
onCheckedChange: (isChecked: boolean) => void
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function DropdownMenuCheckboxItem({
|
||||
checked,
|
||||
disabled = false,
|
||||
onCheckedChange,
|
||||
children,
|
||||
}: MenuCheckboxItemProps): JSX.Element {
|
||||
return (
|
||||
<DMCheckboxItem
|
||||
dir="ltr"
|
||||
className={rowButton({ bp: breakpoints })}
|
||||
onCheckedChange={onCheckedChange}
|
||||
checked={checked}
|
||||
disabled={disabled}
|
||||
>
|
||||
{children}
|
||||
<DMItemIndicator dir="ltr">
|
||||
<div className={iconWrapper({ size: 'small' })}>
|
||||
<CheckIcon />
|
||||
</div>
|
||||
</DMItemIndicator>
|
||||
</DMCheckboxItem>
|
||||
)
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
import css from '~styles'
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Floating Container */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
export const floatingContainer = css({
|
||||
backgroundColor: '$panel',
|
||||
willChange: 'transform',
|
||||
border: '1px solid $panel',
|
||||
borderRadius: '4px',
|
||||
boxShadow: '$4',
|
||||
display: 'flex',
|
||||
height: 'fit-content',
|
||||
padding: '$0',
|
||||
pointerEvents: 'all',
|
||||
position: 'relative',
|
||||
userSelect: 'none',
|
||||
zIndex: 200,
|
||||
variants: {
|
||||
direction: {
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
column: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
},
|
||||
elevation: {
|
||||
0: {
|
||||
boxShadow: 'none',
|
||||
},
|
||||
2: {
|
||||
boxShadow: '$2',
|
||||
},
|
||||
3: {
|
||||
boxShadow: '$3',
|
||||
},
|
||||
4: {
|
||||
boxShadow: '$4',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
|
@ -1,47 +0,0 @@
|
|||
import css from '~styles'
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Icon Wrapper */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
export const iconWrapper = css({
|
||||
height: '100%',
|
||||
borderRadius: '4px',
|
||||
marginRight: '1px',
|
||||
display: 'grid',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
outline: 'none',
|
||||
border: 'none',
|
||||
pointerEvents: 'all',
|
||||
cursor: 'pointer',
|
||||
color: '$text',
|
||||
|
||||
'& svg': {
|
||||
height: 22,
|
||||
width: 22,
|
||||
strokeWidth: 1,
|
||||
},
|
||||
|
||||
'& > *': {
|
||||
gridRow: 1,
|
||||
gridColumn: 1,
|
||||
},
|
||||
|
||||
variants: {
|
||||
size: {
|
||||
small: {
|
||||
'& svg': {
|
||||
height: '16px',
|
||||
width: '16px',
|
||||
},
|
||||
},
|
||||
medium: {
|
||||
'& svg': {
|
||||
height: '22px',
|
||||
width: '22px',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
|
@ -1,13 +0,0 @@
|
|||
export * from './breakpoints'
|
||||
export * from './buttons-row'
|
||||
export * from './context-menu'
|
||||
export * from './dialog'
|
||||
export * from './dropdown-menu'
|
||||
export * from './floating-container'
|
||||
export * from './icon-button'
|
||||
export * from './icon-wrapper'
|
||||
export * from './kbd'
|
||||
export * from './menu'
|
||||
export * from './radio-group'
|
||||
export * from './row-button'
|
||||
export * from './tooltip'
|
|
@ -1,66 +0,0 @@
|
|||
import { breakpoints } from './breakpoints'
|
||||
import css from '~styles'
|
||||
import { rowButton } from './row-button'
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Menu */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
export const menuContent = css({
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
userSelect: 'none',
|
||||
zIndex: 180,
|
||||
minWidth: 180,
|
||||
pointerEvents: 'all',
|
||||
backgroundColor: '$panel',
|
||||
border: '1px solid $panel',
|
||||
padding: '$0',
|
||||
boxShadow: '$4',
|
||||
borderRadius: '4px',
|
||||
font: '$ui',
|
||||
})
|
||||
|
||||
export const divider = css({
|
||||
backgroundColor: '$hover',
|
||||
height: 1,
|
||||
marginTop: '$2',
|
||||
marginRight: '-$2',
|
||||
marginBottom: '$2',
|
||||
marginLeft: '-$2',
|
||||
})
|
||||
|
||||
export function MenuButton({
|
||||
warn,
|
||||
onSelect,
|
||||
children,
|
||||
disabled = false,
|
||||
}: {
|
||||
warn?: boolean
|
||||
onSelect?: () => void
|
||||
disabled?: boolean
|
||||
children: React.ReactNode
|
||||
}): JSX.Element {
|
||||
return (
|
||||
<button
|
||||
className={rowButton({ bp: breakpoints, warn })}
|
||||
disabled={disabled}
|
||||
onSelect={onSelect}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export const menuTextInput = css({
|
||||
backgroundColor: '$panel',
|
||||
border: 'none',
|
||||
padding: '$4 $3',
|
||||
width: '100%',
|
||||
outline: 'none',
|
||||
background: '$input',
|
||||
borderRadius: '4px',
|
||||
fontFamily: '$ui',
|
||||
fontSize: '$1',
|
||||
userSelect: 'all',
|
||||
})
|
|
@ -1,18 +0,0 @@
|
|||
import React from 'react'
|
||||
import css from '~styles'
|
||||
import { Root as RGRoot } from '@radix-ui/react-radio-group'
|
||||
|
||||
/* -------------------------------------------------- */
|
||||
/* Radio Group */
|
||||
/* -------------------------------------------------- */
|
||||
|
||||
export const group = css({
|
||||
display: 'flex',
|
||||
})
|
||||
|
||||
export const Group = React.forwardRef<
|
||||
React.ElementRef<typeof RGRoot>,
|
||||
React.ComponentProps<typeof RGRoot>
|
||||
>((props, forwardedRef) => (
|
||||
<RGRoot {...props} ref={forwardedRef} className={group({ className: props.className })} />
|
||||
))
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue