Starts on menu, page panel

This commit is contained in:
Steve Ruiz 2021-08-16 08:49:31 +01:00
parent 72f680ce65
commit 9c45e0a5a5
46 changed files with 811 additions and 35 deletions

View file

@ -5,7 +5,7 @@ import { Binding } from './binding'
jest.spyOn(console, 'error').mockImplementation(() => void null)
describe('binding', () => {
test('mounts component', () => {
test('mounts component without crashing', () => {
renderWithSvg(<Binding point={[0, 0]} type={'anchor'} />)
})
})

View file

@ -3,7 +3,7 @@ import { renderWithSvg } from '+test'
import { Bounds } from './bounds'
describe('bounds', () => {
test('mounts component', () => {
test('mounts component without crashing', () => {
renderWithSvg(
<Bounds
zoom={1}

View file

@ -3,7 +3,7 @@ import { renderWithSvg } from '+test'
import { Brush } from './brush'
describe('brush', () => {
test('mounts component', () => {
test('mounts component without crashing', () => {
renderWithSvg(<Brush />)
})
})

View file

@ -3,7 +3,7 @@ import { mockDocument, renderWithContext } from '+test'
import { Canvas } from './canvas'
describe('page', () => {
test('mounts component', () => {
test('mounts component without crashing', () => {
renderWithContext(
<Canvas
page={mockDocument.page}

View file

@ -3,7 +3,7 @@ import { renderWithSvg } from '+test'
import { Defs } from './defs'
describe('defs', () => {
test('mounts component', () => {
test('mounts component without crashing', () => {
renderWithSvg(<Defs zoom={1} />)
})
})

View file

@ -3,7 +3,7 @@ import { renderWithContext } from '+test'
import { ErrorFallback } from './error-fallback'
describe('error fallback', () => {
test('mounts component', () => {
test('mounts component without crashing', () => {
renderWithContext(<ErrorFallback error={new Error()} resetErrorBoundary={() => void null} />)
})
})

View file

@ -3,7 +3,7 @@ import { mockUtils, renderWithContext } from '+test'
import { Handles } from './handles'
describe('handles', () => {
test('mounts component', () => {
test('mounts component without crashing', () => {
renderWithContext(<Handles zoom={1} shape={mockUtils.box.create({})} />)
})
})

View file

@ -3,7 +3,7 @@ import { mockDocument, renderWithContext } from '+test'
import { Page } from './page'
describe('page', () => {
test('mounts component', () => {
test('mounts component without crashing', () => {
renderWithContext(
<Page
page={mockDocument.page}

View file

@ -4,7 +4,7 @@ import { render } from '@testing-library/react'
import { Renderer } from './renderer'
describe('context menu', () => {
test('mounts component', () => {
test('mounts component without crashing', () => {
render(
<Renderer
shapeUtils={mockUtils}

View file

@ -3,7 +3,7 @@ import { mockUtils, renderWithSvg } from '+test'
import { ShapeIndicator } from './shape-indicator'
describe('shape indicator', () => {
test('mounts component', () => {
test('mounts component without crashing', () => {
renderWithSvg(<ShapeIndicator shape={mockUtils.box.create({})} variant={'selected'} />)
})
})

View file

@ -3,7 +3,7 @@ import { mockUtils, renderWithSvg } from '+test'
import { Shape } from './shape'
describe('handles', () => {
test('mounts component', () => {
test('mounts component without crashing', () => {
renderWithSvg(
<Shape
shape={mockUtils.box.create({})}

View file

@ -44,6 +44,7 @@
"react-dom": "^17.0.2"
},
"dependencies": {
"@radix-ui/react-alert-dialog": "^0.0.20",
"@radix-ui/react-checkbox": "^0.0.16",
"@radix-ui/react-context-menu": "^0.0.23",
"@radix-ui/react-dropdown-menu": "^0.0.22",

View file

@ -3,7 +3,7 @@ import { ContextMenu } from './context-menu'
import { renderWithContext } from '~test'
describe('context menu', () => {
test('mounts component', () => {
test('mounts component without crashing', () => {
renderWithContext(
<ContextMenu>
<div>Hello</div>

View file

@ -0,0 +1,9 @@
import * as React from 'react'
import { Menu } from './menu'
import { mockDocument, renderWithContext } from '~test'
describe('menu menu', () => {
test('mounts component without crashing', () => {
renderWithContext(<Menu />)
})
})

View file

@ -0,0 +1,126 @@
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,
DropdownMenuCheckboxItem,
IconWrapper,
Kbd,
} from '~components/shared'
import { useTLDrawContext, useTheme } from '~hooks'
import type { Data } from '~types'
export const Menu = React.memo(() => {
const { tlstate } = useTLDrawContext()
const handleNew = React.useCallback(() => {
tlstate.newProject()
}, [tlstate])
const handleSave = React.useCallback(() => {
tlstate.saveProject()
}, [tlstate])
const handleLoad = React.useCallback(() => {
tlstate.loadProject()
}, [tlstate])
const toggleDebugMode = React.useCallback(() => {
tlstate.toggleDebugMode()
}, [tlstate])
const handleSignOut = React.useCallback(() => {
tlstate.signOut()
}, [tlstate])
return (
<FloatingContainer>
<DropdownMenuRoot>
<DropdownMenu.Trigger as={IconButton} bp={breakpoints}>
<HamburgerMenuIcon />
</DropdownMenu.Trigger>
<DropdownMenu.Content as={MenuContent} sideOffset={8} align="end">
<DropdownMenuButton onSelect={handleNew} disabled={true}>
<span>New Project</span>
<Kbd variant="menu">#N</Kbd>
</DropdownMenuButton>
<DropdownMenuDivider />
<DropdownMenuButton onSelect={handleLoad}>
<span>Open...</span>
<Kbd variant="menu">#L</Kbd>
</DropdownMenuButton>
<RecentFiles />
<DropdownMenuDivider />
<DropdownMenuButton onSelect={handleSave}>
<span>Save</span>
<Kbd variant="menu">#S</Kbd>
</DropdownMenuButton>
<DropdownMenuButton onSelect={handleSave}>
<span>Save As...</span>
<Kbd variant="menu">#S</Kbd>
</DropdownMenuButton>
<DropdownMenuDivider />
<Preferences />
<DropdownMenuDivider />
<DropdownMenuButton onSelect={handleSignOut}>
<span>Sign Out</span>
<IconWrapper size="small">
<ExitIcon />
</IconWrapper>
</DropdownMenuButton>
</DropdownMenu.Content>
</DropdownMenuRoot>
</FloatingContainer>
)
})
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>
)
}
const isDebugModeSelector = (s: Data) => s.settings.isDebugMode
function Preferences() {
const { tlstate, useSelector } = useTLDrawContext()
const { theme, setTheme } = useTheme()
const isDebugMode = useSelector(isDebugModeSelector)
const isDarkMode = theme === 'dark'
const toggleDebugMode = React.useCallback(() => {
tlstate.toggleDebugMode()
}, [tlstate])
return (
<DropdownMenuSubMenu label="Preferences">
<DropdownMenuCheckboxItem
checked={isDarkMode}
onCheckedChange={() => setTheme(isDarkMode ? 'light' : 'dark')}
>
<span>Dark Mode</span>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem checked={isDebugMode} onCheckedChange={toggleDebugMode}>
<span>Debug Mode</span>
</DropdownMenuCheckboxItem>
</DropdownMenuSubMenu>
)
}

View file

@ -0,0 +1,20 @@
export function Preferences() {
const { theme, setTheme } = useTheme()
const isDebugMode = useSelector((s) => s.data.settings.isDebugMode)
const isDarkMode = theme === 'dark'
return (
<DropdownMenuSubMenu label="Preferences">
<DropdownMenuCheckboxItem
checked={isDarkMode}
onCheckedChange={() => setTheme(isDarkMode ? 'light' : 'dark')}
>
<span>Dark Mode</span>
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem checked={isDebugMode} onCheckedChange={toggleDebugMode}>
<span>Debug Mode</span>
</DropdownMenuCheckboxItem>
</DropdownMenuSubMenu>
)
}

View file

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

View file

@ -0,0 +1,9 @@
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.page} />)
})
})

View file

@ -0,0 +1,122 @@
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,
MenuTextInput,
DialogInputWrapper,
Divider,
} from '~components/shared'
import type { Data, TLDrawPage } from '~types'
import { useTLDrawContext } from '~hooks'
const canDeleteSelector = (s: Data) => {
// TODO: Include all pages
return [s.page].length <= 1
}
export function PageOptionsDialog({ page }: { page: TLDrawPage }): JSX.Element {
const { tlstate, useSelector } = useTLDrawContext()
const [isOpen, setIsOpen] = React.useState(false)
const canDelete = useSelector(canDeleteSelector)
const rInput = React.useRef<HTMLInputElement>(null)
const [name, setName] = React.useState(page.name || 'Page')
const handleNameChange = React.useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.currentTarget.value)
}, [])
const handleDuplicate = React.useCallback(() => {
tlstate.duplicatePage(page.id)
}, [tlstate])
const handleDelete = React.useCallback(() => {
tlstate.deletePage(page.id)
}, [tlstate])
const handleOpenChange = React.useCallback(
(isOpen: boolean) => {
setIsOpen(isOpen)
if (isOpen) {
return
}
if (name.length === 0) {
tlstate.renamePage(page.id, 'Page')
}
},
[tlstate, name]
)
const handleSave = React.useCallback(() => {
tlstate.renamePage(page.id, name)
}, [tlstate, name])
function stopPropagation(e: React.KeyboardEvent<HTMLDivElement>) {
e.stopPropagation()
}
function handleKeydown(e: React.KeyboardEvent<HTMLDivElement>) {
if (e.key === 'Enter') {
handleSave()
setIsOpen(false)
}
}
React.useEffect(() => {
if (isOpen) {
setTimeout(() => {
rInput.current?.focus()
rInput.current?.select()
}, 0)
}
}, [isOpen])
return (
<Dialog.Root open={isOpen} onOpenChange={handleOpenChange}>
<Dialog.Trigger as={IconButton} bp={breakpoints} size="small" data-shy="true">
<MixerVerticalIcon />
</Dialog.Trigger>
<Dialog.Overlay as={DialogOverlay} />
<Dialog.Content as={DialogContent} onKeyDown={stopPropagation} onKeyUp={stopPropagation}>
<DialogInputWrapper>
<MenuTextInput
ref={rInput}
value={name}
onChange={handleNameChange}
onKeyDown={handleKeydown}
/>
</DialogInputWrapper>
<Divider />
<Dialog.Action as={RowButton} bp={breakpoints} onClick={handleDuplicate}>
Duplicate
</Dialog.Action>
<Dialog.Action
as={RowButton}
bp={breakpoints}
disabled={!canDelete}
onClick={handleDelete}
warn={true}
>
Delete
</Dialog.Action>
<Divider />
<Dialog.Action as={RowButton} bp={breakpoints} onClick={handleSave}>
Save
</Dialog.Action>
<Dialog.Cancel as={RowButton} bp={breakpoints}>
Cancel
</Dialog.Cancel>
</Dialog.Content>
</Dialog.Root>
)
}

View file

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

View file

@ -0,0 +1,9 @@
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

@ -0,0 +1,110 @@
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 styled from '~styles'
import { useTLDrawContext } from '~hooks'
import type { Data } from '~types'
const currentPageSelector = (s: Data) => s.page
export function PagePanel(): JSX.Element {
const rIsOpen = React.useRef(false)
const [isOpen, setIsOpen] = React.useState(false)
const { tlstate, useSelector } = useTLDrawContext()
React.useEffect(() => {
if (rIsOpen.current !== isOpen) {
rIsOpen.current = isOpen
}
}, [isOpen])
const handleCreatePage = React.useCallback(() => {
tlstate.createPage()
}, [tlstate])
const handleChangePage = React.useCallback(
(id: string) => {
setIsOpen(false)
tlstate.changePage(id)
},
[tlstate]
)
const currentPage = useSelector(currentPageSelector)
const sorted = Object.values([currentPage]).sort(
(a, b) => (a.childIndex || 0) - (b.childIndex || 0)
)
return (
<DropdownMenu.Root
dir="ltr"
open={isOpen}
onOpenChange={(isOpen) => {
if (rIsOpen.current !== isOpen) {
setIsOpen(isOpen)
}
}}
>
<FloatingContainer>
<RowButton as={DropdownMenu.Trigger} bp={breakpoints} variant="noIcon">
<span>{currentPage.name || 'Page'}</span>
</RowButton>
</FloatingContainer>
<MenuContent as={DropdownMenu.Content} sideOffset={8} align="start">
<DropdownMenu.RadioGroup value={currentPage.id} onValueChange={handleChangePage}>
{sorted.map((page) => (
<ButtonWithOptions key={page.id}>
<DropdownMenu.RadioItem
as={RowButton}
bp={breakpoints}
value={page.id}
variant="pageButton"
>
<span>{page.name}</span>
<DropdownMenu.ItemIndicator>
<IconWrapper size="small">
<CheckIcon />
</IconWrapper>
</DropdownMenu.ItemIndicator>
</DropdownMenu.RadioItem>
<PageOptionsDialog page={page} />
</ButtonWithOptions>
))}
</DropdownMenu.RadioGroup>
<DropdownMenuDivider />
<DropdownMenuButton onSelect={handleCreatePage}>
<span>Create Page</span>
<IconWrapper size="small">
<PlusIcon />
</IconWrapper>
</DropdownMenuButton>
</MenuContent>
</DropdownMenu.Root>
)
}
const ButtonWithOptions = styled('div', {
display: 'grid',
gridTemplateColumns: '1fr auto',
gridAutoFlow: 'column',
'& > *[data-shy="true"]': {
opacity: 0,
},
'&:hover > *[data-shy="true"]': {
opacity: 1,
},
})

View file

@ -3,7 +3,7 @@ import { renderWithContext } from '~test'
import { StylePanel } from './style-panel'
describe('style panel', () => {
test('mounts component', () => {
test('mounts component without crashing', () => {
renderWithContext(<StylePanel />)
})
})

View file

@ -3,7 +3,7 @@ import { render } from '@testing-library/react'
import { TLDraw } from './tldraw'
describe('tldraw', () => {
test('mounts component', () => {
test('mounts component without crashing', () => {
render(<TLDraw />)
})
})

View file

@ -124,6 +124,7 @@ export function TLDraw({ document, currentPageId, onMount, onChange: _onChange }
onTextKeyUp={tlstate.onTextKeyUp}
/>
</ContextMenu>
<MenuButtons />
<Spacer />
<StylePanel />
<ToolsPanel />
@ -137,10 +138,10 @@ const Spacer = styled('div', {
flexGrow: 2,
})
// const MenuButtons = styled('div', {
// display: 'flex',
// gap: 8,
// })
const MenuButtons = styled('div', {
display: 'flex',
gap: 8,
})
const Layout = styled('main', {
position: 'fixed',

View file

@ -3,7 +3,7 @@ import { ToolsPanel } from './tools-panel'
import { renderWithContext } from '~test'
describe('tools panel', () => {
test('mounts component', () => {
test('mounts component without crashing', () => {
renderWithContext(<ToolsPanel />)
})
})

View file

@ -4,5 +4,6 @@ export function useTheme() {
return {
theme: 'light' as Theme,
toggle: () => null,
setTheme: (theme: Theme) => void theme,
}
}

View file

@ -0,0 +1,28 @@
import { TLDrawState } from '~state'
import { mockDocument } from '~test'
describe('Change page command', () => {
const tlstate = new TLDrawState()
it('does, undoes and redoes command', () => {
tlstate.loadDocument(mockDocument)
const initialId = tlstate.page.id
tlstate.createPage()
const nextId = tlstate.page.id
tlstate.changePage(initialId)
expect(tlstate.page.id).toBe(initialId)
tlstate.undo()
expect(tlstate.page.id).toBe(nextId)
tlstate.redo()
expect(tlstate.page.id).toBe(initialId)
})
})

View file

@ -0,0 +1,10 @@
import type { TLDrawShape, Data, Command } from '~types'
import { TLDR } from '~state/tldr'
export function changePage(data: Data): Command {
return {
id: 'create_page',
before: {},
after: {},
}
}

View file

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

View file

@ -0,0 +1,26 @@
import { TLDrawState } from '~state'
import { mockDocument } from '~test'
describe('Create page command', () => {
const tlstate = new TLDrawState()
it('does, undoes and redoes command', () => {
tlstate.loadDocument(mockDocument)
const initialId = tlstate.page.id
tlstate.createPage()
const nextId = tlstate.page.id
expect(tlstate.page.id).toBe(nextId)
tlstate.undo()
expect(tlstate.page.id).toBe(initialId)
tlstate.redo()
expect(tlstate.page.id).toBe(nextId)
})
})

View file

@ -0,0 +1,10 @@
import type { TLDrawShape, Data, Command } from '~types'
import { TLDR } from '~state/tldr'
export function createPage(data: Data): Command {
return {
id: 'create_page',
before: {},
after: {},
}
}

View file

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

View file

@ -0,0 +1,28 @@
import { TLDrawState } from '~state'
import { mockDocument } from '~test'
describe('Delete page', () => {
const tlstate = new TLDrawState()
it('does, undoes and redoes command', () => {
tlstate.loadDocument(mockDocument)
const initialId = tlstate.page.id
tlstate.createPage()
const nextId = tlstate.page.id
tlstate.deletePage()
expect(tlstate.page.id).toBe(nextId)
tlstate.undo()
expect(tlstate.page.id).toBe(initialId)
tlstate.redo()
expect(tlstate.page.id).toBe(nextId)
})
})

View file

@ -0,0 +1,10 @@
import type { TLDrawShape, Data, Command } from '~types'
import { TLDR } from '~state/tldr'
export function deletePage(data: Data, id: string): Command {
return {
id: 'delete_page',
before: {},
after: {},
}
}

View file

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

View file

@ -0,0 +1,24 @@
import { TLDrawState } from '~state'
import { mockDocument } from '~test'
describe('Duplicate page', () => {
const tlstate = new TLDrawState()
it('does, undoes and redoes command', () => {
tlstate.loadDocument(mockDocument)
const initialId = tlstate.page.id
tlstate.duplicatePage()
const nextId = tlstate.page.id
tlstate.undo()
expect(tlstate.page.id).toBe(initialId)
tlstate.redo()
expect(tlstate.page.id).toBe(nextId)
})
})

View file

@ -0,0 +1,10 @@
import type { TLDrawShape, Data, Command } from '~types'
import { TLDR } from '~state/tldr'
export function duplicatePage(data: Data, id: string): Command {
return {
id: 'duplicate_page',
before: {},
after: {},
}
}

View file

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

View file

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

View file

@ -0,0 +1,25 @@
import { TLDrawState } from '~state'
import { mockDocument } from '~test'
describe('Edit page', () => {
const tlstate = new TLDrawState()
it('does, undoes and redoes command', () => {
tlstate.loadDocument(mockDocument)
const initialId = tlstate.page.id
const initialName = tlstate.page.name
tlstate.renamePage(initialId, 'My Special Page')
expect(tlstate.page.name).toBe('My Special Page')
tlstate.undo()
expect(tlstate.page.name).toBe(initialName)
tlstate.redo()
expect(tlstate.page.name).toBe('My Special Page')
})
})

View file

@ -0,0 +1,10 @@
import type { TLDrawShape, Data, Command } from '~types'
import { TLDR } from '~state/tldr'
export function editPage(data: Data, id: string): Command {
return {
id: 'edit_page',
before: {},
after: {},
}
}

View file

@ -519,7 +519,22 @@ export class TLDrawState implements TLCallbacks {
return this
}
/* ---------------------- Document --------------------- */
/* -------------------------------------------------- */
/* Document */
/* -------------------------------------------------- */
private setCurrentPageId(pageId: string) {
if (pageId === this.currentPageId) return this
this.currentPageId = pageId
this.setState({
page: this.pages[pageId],
pageState: this.pageStates[pageId],
})
return this
}
loadDocument = (document: TLDrawDocument, onChange?: TLDrawState['_onChange']) => {
this._onChange = onChange
this.currentDocumentId = document.id
@ -543,19 +558,26 @@ export class TLDrawState implements TLCallbacks {
return this
}
setCurrentPageId(pageId: string) {
if (pageId === this.currentPageId) return this
this.currentPageId = pageId
this.setState({
page: this.pages[pageId],
pageState: this.pageStates[pageId],
})
return this
newProject = () => {
// TODO
}
/* -------------------- Sessions -------------------- */
saveProject = () => {
// TODO
}
loadProject = () => {
// TODO
}
signOut = () => {
// TODO
}
/* -------------------------------------------------- */
/* Sessions */
/* -------------------------------------------------- */
startSession<T extends Session>(session: T, ...args: ParametersExceptFirst<T['start']>) {
this.session = session
this.setState((data) => session.start(data, ...args), session.status)
@ -658,7 +680,10 @@ export class TLDrawState implements TLCallbacks {
return this
}
/* -------------------- Commands -------------------- */
/* -------------------------------------------------- */
/* History */
/* -------------------------------------------------- */
do(command: Command) {
const { history } = this
@ -739,7 +764,10 @@ export class TLDrawState implements TLCallbacks {
return this
}
/* -------------------- Selection ------------------- */
/* -------------------------------------------------- */
/* Selection */
/* -------------------------------------------------- */
setSelectedIds(ids: string[], push = false) {
this.setState((data) => {
return {
@ -936,6 +964,10 @@ export class TLDrawState implements TLCallbacks {
return this
}
toggleDebugMode = () => {
// TODO
}
rotate = (delta = Math.PI * -0.5, ids?: string[]) => {
const data = this.store.getState()
const idsToMutate = ids ? ids : data.pageState.selectedIds
@ -1006,6 +1038,52 @@ export class TLDrawState implements TLCallbacks {
return this
}
createPage() {
const newId = Utils.uniqueId()
this.pages[newId] = { id: newId, shapes: {}, bindings: {} }
this.changePage(newId)
return this
}
changePage(id: string) {
this.setCurrentPageId(id)
return this
}
renamePage(id: string, name: string) {
this.pages[id] = { ...this.pages[id], name }
return this
}
duplicatePage(id: string = this.currentPageId) {
const newId = Utils.uniqueId()
this.pages[newId] = { ...this.pages[id], id: newId }
this.changePage(newId)
return this
}
deletePage(id: string = this.currentPageId) {
const pages = Object.values(this.pages).sort(
(a, b) => (a.childIndex || 0) - (b.childIndex || 0)
)
const currentIndex = pages.findIndex((page) => page.id === this.currentPageId)
if (Object.values(this.pages).length <= 1) return
delete this.pages[id]
if (id === this.currentPageId) {
if (currentIndex === pages.length - 1) {
this.changePage(pages[pages.length - 2].id)
} else {
this.changePage(pages[currentIndex + 1].id)
}
}
return this
}
copy = (ids?: string[]) => {
const data = this.store.getState()
const idsToCopy = ids ? ids : data.pageState.selectedIds

View file

@ -8,9 +8,11 @@ import type { StoreApi } from 'zustand'
export type TLStore = StoreApi<Data>
export type TLChange = Data
export type TLDrawPage = TLPage<TLDrawShape, TLDrawBinding>
export interface TLDrawDocument {
id: string
pages: Record<string, TLPage<TLDrawShape, TLDrawBinding>>
pages: Record<string, TLDrawPage>
pageStates: Record<string, TLPageState>
}

100
yarn.lock
View file

@ -1634,6 +1634,20 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-alert-dialog@^0.0.20":
version "0.0.20"
resolved "https://registry.yarnpkg.com/@radix-ui/react-alert-dialog/-/react-alert-dialog-0.0.20.tgz#1adb997c899fd9cb6f0d13bc66b50d2166414339"
integrity sha512-Vaz16wc4rDVHC7BH2At3TM+HEU56jN6fAWoeXatZyv1BAaehSabusbghC4V8Cfc0llP4ijvOY7Eznkt0+jP/fQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "0.0.5"
"@radix-ui/react-compose-refs" "0.0.5"
"@radix-ui/react-context" "0.0.5"
"@radix-ui/react-dialog" "0.0.20"
"@radix-ui/react-polymorphic" "0.0.13"
"@radix-ui/react-primitive" "0.0.15"
"@radix-ui/react-slot" "0.0.12"
"@radix-ui/react-arrow@0.0.14":
version "0.0.14"
resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-0.0.14.tgz#70b2c66efbf3cde0c9dd0895417e39f6cdf31805"
@ -1696,6 +1710,28 @@
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-dialog@0.0.20":
version "0.0.20"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-0.0.20.tgz#b26607bea68fc20067d06fab996bac7f1acf68c1"
integrity sha512-fXgWxWyvmNiimxrFGdvUNve0tyQEFyPwrNgkSi6Xiha9cX8sqWdiYWq500zhzUQQFJVS7No73ylx8kgrI7SoLw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "0.0.5"
"@radix-ui/react-compose-refs" "0.0.5"
"@radix-ui/react-context" "0.0.5"
"@radix-ui/react-dismissable-layer" "0.0.15"
"@radix-ui/react-focus-guards" "0.0.7"
"@radix-ui/react-focus-scope" "0.0.15"
"@radix-ui/react-id" "0.0.6"
"@radix-ui/react-polymorphic" "0.0.13"
"@radix-ui/react-portal" "0.0.15"
"@radix-ui/react-presence" "0.0.15"
"@radix-ui/react-primitive" "0.0.15"
"@radix-ui/react-slot" "0.0.12"
"@radix-ui/react-use-controllable-state" "0.0.6"
aria-hidden "^1.1.1"
react-remove-scroll "^2.4.0"
"@radix-ui/react-dismissable-layer@0.0.14":
version "0.0.14"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-0.0.14.tgz#9d8a3415a2830688070c6596dec18b43c33df7b2"
@ -1709,6 +1745,19 @@
"@radix-ui/react-use-callback-ref" "0.0.5"
"@radix-ui/react-use-escape-keydown" "0.0.6"
"@radix-ui/react-dismissable-layer@0.0.15":
version "0.0.15"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-0.0.15.tgz#02c0e68684d60933c82b5af6793c87a5f9ee0750"
integrity sha512-2zABi8rh/t6liFfRLBw6h+B7MNNFxVQrgYfWRMs1elNX41z3G2vLoBlWdqGzAlYrtqEr/6CL4pQfhwVtd7rNGw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "0.0.5"
"@radix-ui/react-polymorphic" "0.0.13"
"@radix-ui/react-primitive" "0.0.15"
"@radix-ui/react-use-body-pointer-events" "0.0.7"
"@radix-ui/react-use-callback-ref" "0.0.5"
"@radix-ui/react-use-escape-keydown" "0.0.6"
"@radix-ui/react-dropdown-menu@^0.0.22":
version "0.0.22"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-0.0.22.tgz#914dbbb12d31b4379df697f1262150b3f50916ae"
@ -1742,6 +1791,17 @@
"@radix-ui/react-primitive" "0.0.14"
"@radix-ui/react-use-callback-ref" "0.0.5"
"@radix-ui/react-focus-scope@0.0.15":
version "0.0.15"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-0.0.15.tgz#60917075e53ee72d2a473fba88eb31e7aaf7d841"
integrity sha512-zNgEe1lyLPfxa003VD8lCXaadGqCYhboA3X1WDNGes74lzJgLOPJgzLI0F/ksSokkx/yDDdReyOWui3/LCTqTw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "0.0.5"
"@radix-ui/react-polymorphic" "0.0.13"
"@radix-ui/react-primitive" "0.0.15"
"@radix-ui/react-use-callback-ref" "0.0.5"
"@radix-ui/react-icons@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@radix-ui/react-icons/-/react-icons-1.0.3.tgz#4ef61f1234f44991f7a19e108f77ca37032b4be2"
@ -1796,6 +1856,11 @@
resolved "https://registry.yarnpkg.com/@radix-ui/react-polymorphic/-/react-polymorphic-0.0.12.tgz#bf4ae516669b68e059549538104d97322f7c876b"
integrity sha512-/GYNMicBnGzjD1d2fCAuzql1VeFrp8mqM3xfzT1kxhnV85TKdURO45jBfMgqo17XNXoNhWIAProUsCO4qFAAIg==
"@radix-ui/react-polymorphic@0.0.13":
version "0.0.13"
resolved "https://registry.yarnpkg.com/@radix-ui/react-polymorphic/-/react-polymorphic-0.0.13.tgz#d010d48281626191c9513f11db5d82b37662418a"
integrity sha512-0sGqBp+v9/yrsMhPfAejxcem2MwAFgaSAxF3Sieaklm6ZVYM/hTZxxWI5NVOLGV+482GwW0wIqwpVUzREjmh+w==
"@radix-ui/react-popper@0.0.17":
version "0.0.17"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-0.0.17.tgz#a73486e19a628cb3fecaf3fb6eecf6e2cab9d0be"
@ -1822,6 +1887,16 @@
"@radix-ui/react-primitive" "0.0.14"
"@radix-ui/react-use-layout-effect" "0.0.5"
"@radix-ui/react-portal@0.0.15":
version "0.0.15"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-0.0.15.tgz#833bccb192aafb9420bd037d5827e88caf429dc4"
integrity sha512-qMESsdqph1gbRGzy9oSzUoeZYXnR2egXVcEZDqmesfn8w/o1rC1wadKkyBf7qo/YyjUX4mvXknAA+ftp1aQp+w==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-polymorphic" "0.0.13"
"@radix-ui/react-primitive" "0.0.15"
"@radix-ui/react-use-layout-effect" "0.0.5"
"@radix-ui/react-presence@0.0.14":
version "0.0.14"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-0.0.14.tgz#6a86058bbbf46234dd8840dacd620b3ac5797025"
@ -1830,6 +1905,15 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "0.0.5"
"@radix-ui/react-presence@0.0.15":
version "0.0.15"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-0.0.15.tgz#4ff12feb436f1499148feb11c3a63a5d8fab568a"
integrity sha512-+5+ePKUdTkqN1ze7nYmcoeHSsmKCcREwt0NhvNgDocPaqEUoZSkK9Mq6eMiMXSj02NkXH9P+bK32VCClYFnMBQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-compose-refs" "0.0.5"
"@radix-ui/react-use-layout-effect" "0.0.5"
"@radix-ui/react-primitive@0.0.14":
version "0.0.14"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-0.0.14.tgz#752a967cb05d4c5643634fe20274e7dc905d1cce"
@ -1838,6 +1922,14 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-polymorphic" "0.0.12"
"@radix-ui/react-primitive@0.0.15":
version "0.0.15"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-0.0.15.tgz#c0cf609ee565a32969d20943e2697b42a04fbdf3"
integrity sha512-Y7JLnen/G3AT0cQXXkBo3A1OuWaKGerkd2gKs0Fuqxv+kTxEmYoqSp/soo0Mm3Ccw61LKLQAjPiE37GK9/Zqwg==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-polymorphic" "0.0.13"
"@radix-ui/react-radio-group@^0.0.18":
version "0.0.18"
resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-0.0.18.tgz#d6f9ce132102deb23ee782e08f7b3e185ea317f0"
@ -1912,6 +2004,14 @@
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect" "0.0.5"
"@radix-ui/react-use-body-pointer-events@0.0.7":
version "0.0.7"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-body-pointer-events/-/react-use-body-pointer-events-0.0.7.tgz#e4249690ca0db85c969400e867476206feda4d1e"
integrity sha512-mXAGyb8mhVjRqtpKPeZePuvee40bgsWpt378oQrIcLU1uZNbNX9eyrIPnnL9OMLAvxqloAOClVj0PZ1bMQmfDw==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/react-use-layout-effect" "0.0.5"
"@radix-ui/react-use-callback-ref@0.0.5":
version "0.0.5"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-0.0.5.tgz#fa8db050229cda573dfeeae213d74ef06f6130db"