[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:
Steve Ruiz 2021-11-03 16:46:33 +00:00 committed by GitHub
parent b68a4681e1
commit e2369003c6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
139 changed files with 2524 additions and 3066 deletions

View file

View 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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View file

@ -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"
}
}

View file

@ -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
})
})

View file

@ -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', () => {

View file

@ -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',
},
})

View 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>
)
}

View 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>
)
}

View file

@ -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>
)
}

View file

@ -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', () => {

View 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',
})

View file

@ -0,0 +1 @@
export * from './ContextMenu'

View 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',
})

View file

@ -0,0 +1 @@
export * from './Divider'

View 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 })

View file

@ -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>
)
}

View 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,
},
},
},
})

View 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',
})

View 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>
)
}

View 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>
)
}

View 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',
},
})

View 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>
)
}

View file

@ -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>
)
}

View 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'

View 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',
},
})

View 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,

View file

@ -0,0 +1 @@
export * from './IconButton'

View file

@ -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',

View file

@ -0,0 +1 @@
export * from './Kbd'

View 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',
})

View file

@ -0,0 +1 @@
export * from './MenuContent'

View 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',
},
},
},
})

View file

@ -0,0 +1 @@
export * from './Panel'

View 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,
},
},
},
},
},
})

View file

@ -0,0 +1 @@
export * from './RowButton'

View 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,
},
})

View file

@ -0,0 +1 @@
export * from './SmallIcon'

View 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',
},
},
},
},
})

View file

@ -0,0 +1 @@
export * from './ToolButton'

View 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,
})

View file

@ -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',

View 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>
)
}

View file

@ -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>
)
})

View file

@ -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',
})

View file

@ -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', () => {

View 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',
})

View file

@ -0,0 +1 @@
export * from './ToolsPanel'

View file

@ -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',
})

View file

@ -0,0 +1 @@
export * from './Tooltip'

View 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>
)
})

View 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>
)
}

View 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>
)
})

View 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>
)
})

View file

@ -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,
})

View 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>
)
}

View 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>
)
}

View 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>
)
})

View 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,
})

View 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,
})

View file

@ -0,0 +1 @@
export * from './TopPanel'

View file

@ -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>
)
}

View file

@ -1 +0,0 @@
export * from './context-menu'

View 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>
)
}

View file

@ -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

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View file

@ -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

View 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>
)
}

View 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>
)
}

View 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>
)
}

View file

@ -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

View file

@ -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

View 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'

View file

@ -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'

View file

@ -1,5 +0,0 @@
export * from './shared/tooltip'
export * from './shared/kbd'
export * from './shared'
export * from './icons'
export * from './tldraw'

View file

@ -1 +0,0 @@
export * from './menu'

View file

@ -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 />)
})
})

View file

@ -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>
)
}

View file

@ -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>
)
}

View file

@ -1 +0,0 @@
export * from './page-options-dialog'

View file

@ -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} />)
})
})

View file

@ -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>
)
}

View file

@ -1 +0,0 @@
export * from './page-panel'

View file

@ -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 />)
})
})

View file

@ -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,
})

View file

@ -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>
)
}

View file

@ -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',
},
})

View file

@ -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>
)
}

View file

@ -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',
},
},
},
})

View file

@ -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',
},
},
},
},
})

View file

@ -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'

View file

@ -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',
})

View file

@ -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