Adds indicator for dots, rename and delete dialogs for pages, dark mode support for ui

This commit is contained in:
Steve Ruiz 2021-08-30 14:04:12 +01:00
parent 62a3da0498
commit 2937016ae0
12 changed files with 127 additions and 86 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -200,7 +200,8 @@ export function useKeyboardShortcuts(tlstate: TLDrawState) {
tlstate.moveToFront()
})
useHotkeys('command+shift+backspace', () => {
useHotkeys('command+shift+backspace', (e) => {
tlstate.reset()
e.preventDefault()
})
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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