Adds indicator for dots, rename and delete dialogs for pages, dark mode support for ui
This commit is contained in:
parent
62a3da0498
commit
2937016ae0
12 changed files with 127 additions and 86 deletions
|
@ -86,22 +86,25 @@ const initialDoc: TLDrawDocument = {
|
|||
}
|
||||
|
||||
export default function Editor(): JSX.Element {
|
||||
const { value, setValue, status } = usePersistence('doc', initialDoc)
|
||||
// const { value, setValue, status } = usePersistence('doc', initialDoc)
|
||||
|
||||
const handleChange = React.useCallback(
|
||||
(tlstate: TLDrawState, patch: TLDrawPatch, reason: string) => {
|
||||
if (reason.startsWith('session')) {
|
||||
return
|
||||
}
|
||||
|
||||
setValue(tlstate.document)
|
||||
},
|
||||
[setValue]
|
||||
)
|
||||
|
||||
if (status === 'loading' || value === null) {
|
||||
return <div />
|
||||
}
|
||||
|
||||
return <TLDraw document={value} onChange={handleChange} />
|
||||
// const handleChange = React.useCallback(
|
||||
// (tlstate: TLDrawState, patch: TLDrawPatch, reason: string) => {
|
||||
// if (reason.startsWith('session')) {
|
||||
// return
|
||||
// }
|
||||
|
||||
// setValue(tlstate.document)
|
||||
// },
|
||||
// [setValue]
|
||||
// )
|
||||
|
||||
// if (status === 'loading' || value === null) {
|
||||
// return <div />
|
||||
// }
|
||||
|
||||
// return <TLDraw document={value} onChange={handleChange} />
|
||||
|
||||
// Will automatically persist data under the provided id, too
|
||||
return <TLDraw id="tldraw" />
|
||||
}
|
||||
|
|
|
@ -58,7 +58,7 @@
|
|||
"ismobilejs": "^1.1.1",
|
||||
"perfect-freehand": "^0.5.2",
|
||||
"react-hotkeys-hook": "^3.4.0",
|
||||
"rko": "^0.5.18"
|
||||
"rko": "^0.5.19"
|
||||
},
|
||||
"gitHead": "4a7439ddf81b615ee49fddbe00802699975f9375"
|
||||
}
|
|
@ -7,8 +7,6 @@ import {
|
|||
DialogOverlay,
|
||||
DialogContent,
|
||||
RowButton,
|
||||
MenuTextInput,
|
||||
DialogInputWrapper,
|
||||
Divider,
|
||||
} from '~components/shared'
|
||||
import type { Data, TLDrawPage } from '~types'
|
||||
|
@ -21,9 +19,10 @@ const canDeleteSelector = (s: Data) => {
|
|||
interface PageOptionsDialogProps {
|
||||
page: TLDrawPage
|
||||
onOpen?: () => void
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
export function PageOptionsDialog({ page, onOpen }: PageOptionsDialogProps): JSX.Element {
|
||||
export function PageOptionsDialog({ page, onOpen, onClose }: PageOptionsDialogProps): JSX.Element {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
|
||||
const [isOpen, setIsOpen] = React.useState(false)
|
||||
|
@ -32,18 +31,16 @@ export function PageOptionsDialog({ page, onOpen }: PageOptionsDialogProps): JSX
|
|||
|
||||
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)
|
||||
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(
|
||||
|
@ -54,27 +51,18 @@ export function PageOptionsDialog({ page, onOpen }: PageOptionsDialogProps): JSX
|
|||
onOpen?.()
|
||||
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)
|
||||
}
|
||||
// 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(() => {
|
||||
|
@ -93,15 +81,9 @@ export function PageOptionsDialog({ page, onOpen }: PageOptionsDialogProps): JSX
|
|||
</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={handleRename}>
|
||||
Rename
|
||||
</Dialog.Action>
|
||||
<Dialog.Action as={RowButton} bp={breakpoints} onClick={handleDuplicate}>
|
||||
Duplicate
|
||||
</Dialog.Action>
|
||||
|
@ -115,9 +97,6 @@ export function PageOptionsDialog({ page, onOpen }: PageOptionsDialogProps): JSX
|
|||
Delete
|
||||
</Dialog.Action>
|
||||
<Divider />
|
||||
<Dialog.Action as={RowButton} bp={breakpoints} onClick={handleSave}>
|
||||
Save
|
||||
</Dialog.Action>
|
||||
<Dialog.Cancel as={RowButton} bp={breakpoints}>
|
||||
Cancel
|
||||
</Dialog.Cancel>
|
||||
|
|
|
@ -100,7 +100,7 @@ function PageMenuContent({ onClose }: { onClose: () => void }) {
|
|||
</IconWrapper>
|
||||
</DropdownMenu.ItemIndicator>
|
||||
</DropdownMenu.RadioItem>
|
||||
<PageOptionsDialog page={page} />
|
||||
<PageOptionsDialog page={page} onClose={onClose} />
|
||||
</ButtonWithOptions>
|
||||
))}
|
||||
</DropdownMenu.RadioGroup>
|
||||
|
|
|
@ -12,25 +12,51 @@ import { ToolsPanel } from '~components/tools-panel'
|
|||
import { PagePanel } from '~components/page-panel'
|
||||
import { Menu } from '~components/menu'
|
||||
|
||||
export interface TLDrawProps {
|
||||
document?: TLDrawDocument
|
||||
currentPageId?: string
|
||||
onMount?: (state: TLDrawState) => void
|
||||
onChange?: TLDrawState['_onChange']
|
||||
}
|
||||
|
||||
// Selectors
|
||||
const isInSelectSelector = (s: Data) => s.appState.activeTool === 'select'
|
||||
|
||||
const isSelectedShapeWithHandlesSelector = (s: Data) => {
|
||||
const { shapes } = s.document.pages[s.appState.currentPageId]
|
||||
const { selectedIds } = s.document.pageStates[s.appState.currentPageId]
|
||||
return selectedIds.length === 1 && selectedIds.every((id) => shapes[id].handles !== undefined)
|
||||
}
|
||||
|
||||
const pageSelector = (s: Data) => s.document.pages[s.appState.currentPageId]
|
||||
|
||||
const pageStateSelector = (s: Data) => s.document.pageStates[s.appState.currentPageId]
|
||||
|
||||
const isDarkModeSelector = (s: Data) => s.settings.isDarkMode
|
||||
|
||||
export function TLDraw({ document, currentPageId, onMount, onChange: _onChange }: TLDrawProps) {
|
||||
const [tlstate] = React.useState(() => new TLDrawState())
|
||||
export interface TLDrawProps {
|
||||
/**
|
||||
* (optional) If provided, the component will load / persist state under this key.
|
||||
*/
|
||||
id?: string
|
||||
/**
|
||||
* (optional) The document to load or update from.
|
||||
*/
|
||||
document?: TLDrawDocument
|
||||
/**
|
||||
* (optional) The current page id.
|
||||
*/
|
||||
currentPageId?: string
|
||||
/**
|
||||
* (optional) A callback to run when the component mounts.
|
||||
*/
|
||||
onMount?: (state: TLDrawState) => void
|
||||
/**
|
||||
* (optional) A callback to run when the component's state changes.
|
||||
*/
|
||||
onChange?: TLDrawState['_onChange']
|
||||
}
|
||||
|
||||
export function TLDraw({ id, document, currentPageId, onMount, onChange: _onChange }: TLDrawProps) {
|
||||
const [tlstate, setTlstate] = React.useState(() => new TLDrawState(id))
|
||||
|
||||
React.useEffect(() => {
|
||||
setTlstate(new TLDrawState(id))
|
||||
}, [id])
|
||||
|
||||
const [context] = React.useState(() => {
|
||||
return { tlstate, useSelector: tlstate.useStore }
|
||||
})
|
||||
|
@ -38,10 +64,15 @@ export function TLDraw({ document, currentPageId, onMount, onChange: _onChange }
|
|||
useKeyboardShortcuts(tlstate)
|
||||
|
||||
const page = context.useSelector(pageSelector)
|
||||
|
||||
const pageState = context.useSelector(pageStateSelector)
|
||||
|
||||
const isDarkMode = context.useSelector(isDarkModeSelector)
|
||||
|
||||
const isSelecting = context.useSelector(isInSelectSelector)
|
||||
|
||||
const isSelectedHandlesShape = context.useSelector(isSelectedShapeWithHandlesSelector)
|
||||
|
||||
const isInSession = !!tlstate.session
|
||||
|
||||
// Hide bounds when not using the select tool, or when the only selected shape has handles
|
||||
|
|
|
@ -200,7 +200,8 @@ export function useKeyboardShortcuts(tlstate: TLDrawState) {
|
|||
tlstate.moveToFront()
|
||||
})
|
||||
|
||||
useHotkeys('command+shift+backspace', () => {
|
||||
useHotkeys('command+shift+backspace', (e) => {
|
||||
tlstate.reset()
|
||||
e.preventDefault()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,9 +1,25 @@
|
|||
import type { Theme } from '~types'
|
||||
import React from 'react'
|
||||
import type { Data, Theme } from '~types'
|
||||
import { useTLDrawContext } from './useTLDrawContext'
|
||||
import { dark } from '~styles'
|
||||
|
||||
const themeSelector = (data: Data): Theme => (data.settings.isDarkMode ? 'dark' : 'light')
|
||||
|
||||
export function useTheme() {
|
||||
const { tlstate, useSelector } = useTLDrawContext()
|
||||
|
||||
const theme = useSelector(themeSelector)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (theme === 'dark') {
|
||||
document.body.classList.add(dark)
|
||||
} else {
|
||||
document.body.classList.remove(dark)
|
||||
}
|
||||
}, [theme])
|
||||
|
||||
return {
|
||||
theme: 'light' as Theme,
|
||||
toggle: () => null,
|
||||
setTheme: (theme: Theme) => void theme,
|
||||
theme,
|
||||
toggle: tlstate.toggleDarkMode,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,7 +47,9 @@ export class Draw extends TLDrawShapeUtil<DrawShape> {
|
|||
// For very short lines, draw a point instead of a line
|
||||
const bounds = this.getBounds(shape)
|
||||
|
||||
if (!isEditing && bounds.width < strokeWidth / 2 && bounds.height < strokeWidth / 2) {
|
||||
const verySmall = bounds.width < strokeWidth / 2 && bounds.height < strokeWidth / 2
|
||||
|
||||
if (!isEditing && verySmall) {
|
||||
const sw = strokeWidth * 0.618
|
||||
|
||||
return (
|
||||
|
@ -151,6 +153,14 @@ export class Draw extends TLDrawShapeUtil<DrawShape> {
|
|||
renderIndicator(shape: DrawShape): JSX.Element {
|
||||
const { points } = shape
|
||||
|
||||
const bounds = this.getBounds(shape)
|
||||
|
||||
const verySmall = bounds.width < 4 && bounds.height < 4
|
||||
|
||||
if (verySmall) {
|
||||
return <circle x={bounds.width / 2} y={bounds.height / 2} r={1} />
|
||||
}
|
||||
|
||||
const path = Utils.getFromCache(this.simplePathCache, points, () => getSolidStrokePath(shape))
|
||||
|
||||
return <path d={path} />
|
||||
|
|
|
@ -157,17 +157,14 @@ describe('Arrow session', () => {
|
|||
expect(tlstate.getShape<ArrowShape>('arrow1').point).toStrictEqual([116, 116])
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.point).toStrictEqual([0, 0])
|
||||
expect(tlstate.getShape<ArrowShape>('arrow1').handles.end.point).toStrictEqual([85, 85])
|
||||
// tlstate
|
||||
// .select('target1')
|
||||
// .startTranslateSession([50, 50])
|
||||
// .updateTranslateSession([300, 0])
|
||||
// .completeSession()
|
||||
// expect(tlstate.getShape<ArrowShape>('arrow1').handles.start.point).toStrictEqual([66.493, 0])
|
||||
// expect(tlstate.getShape<ArrowShape>('arrow1').handles.end.point).toStrictEqual([0, 135])
|
||||
})
|
||||
|
||||
it('updates the arrow when bound on both sides', () => {
|
||||
// TODO
|
||||
})
|
||||
|
||||
it('snaps the bend to zero when dragging the bend handle toward the center', () => {
|
||||
// TODO
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -91,8 +91,8 @@ const initialData: Data = {
|
|||
export class TLDrawState extends StateManager<Data> {
|
||||
_onChange?: (tlstate: TLDrawState, data: Data, reason: string) => void
|
||||
|
||||
constructor() {
|
||||
super(initialData, 'tlstate', 1)
|
||||
constructor(id = Utils.uniqueId()) {
|
||||
super(initialData, id, 1)
|
||||
}
|
||||
|
||||
selectHistory: SelectHistory = {
|
||||
|
@ -258,25 +258,31 @@ export class TLDrawState extends StateManager<Data> {
|
|||
}
|
||||
|
||||
toggleDarkMode = (): this => {
|
||||
return this.patchState(
|
||||
this.patchState(
|
||||
{ settings: { isDarkMode: !this.state.settings.isDarkMode } },
|
||||
`settings:toggled_dark_mode`
|
||||
)
|
||||
this.persist()
|
||||
return this
|
||||
}
|
||||
|
||||
toggleDebugMode = () => {
|
||||
return this.patchState(
|
||||
this.patchState(
|
||||
{ settings: { isDebugMode: !this.state.settings.isDebugMode } },
|
||||
`settings:toggled_debug`
|
||||
)
|
||||
this.persist()
|
||||
return this
|
||||
}
|
||||
|
||||
/* ----------------------- UI ----------------------- */
|
||||
toggleStylePanel = (): this => {
|
||||
return this.patchState(
|
||||
this.patchState(
|
||||
{ appState: { isStyleOpen: !this.appState.isStyleOpen } },
|
||||
'ui:toggled_style_panel'
|
||||
)
|
||||
this.persist()
|
||||
return this
|
||||
}
|
||||
|
||||
selectTool = (tool: TLDrawShapeType | 'select'): this => {
|
||||
|
|
|
@ -99,8 +99,6 @@ const { styled, css, theme, getCssString } = createCss({
|
|||
},
|
||||
})
|
||||
|
||||
const light = theme({})
|
||||
|
||||
const dark = theme({
|
||||
colors: {
|
||||
brushFill: 'rgba(180, 180, 180, .05)',
|
||||
|
@ -138,4 +136,4 @@ const dark = theme({
|
|||
|
||||
export default styled
|
||||
|
||||
export { css, getCssString, light, dark }
|
||||
export { css, getCssString, dark }
|
||||
|
|
|
@ -9469,10 +9469,10 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
|
|||
hash-base "^3.0.0"
|
||||
inherits "^2.0.1"
|
||||
|
||||
rko@^0.5.18:
|
||||
version "0.5.18"
|
||||
resolved "https://registry.yarnpkg.com/rko/-/rko-0.5.18.tgz#cbbc45f073b1db1884112479b18ed04a799065ec"
|
||||
integrity sha512-zh6/NRIZi0gApYMyyJjTLpni/98dmkj/oZhD/KRS2TYZD0V63iIopBWUmD5wSNVVqyOv5o/HHsM1Q4EzdTGeZA==
|
||||
rko@^0.5.19:
|
||||
version "0.5.19"
|
||||
resolved "https://registry.yarnpkg.com/rko/-/rko-0.5.19.tgz#33577596167178abc30063b6dd0a8bbde0362c27"
|
||||
integrity sha512-0KSdDnbhD11GCDvZFARvCJedJuwWIh5F1cCqTGljFF/wQi9PUVu019qH4ME4LdPF3HotMLcdQsxEXmIDeeD0zQ==
|
||||
dependencies:
|
||||
idb-keyval "^5.1.3"
|
||||
zustand "^3.5.9"
|
||||
|
|
Loading…
Reference in a new issue