From 2937016ae0c54e8ad0a82345d71a0988cdfa54df Mon Sep 17 00:00:00 2001 From: Steve Ruiz Date: Mon, 30 Aug 2021 14:04:12 +0100 Subject: [PATCH] Adds indicator for dots, rename and delete dialogs for pages, dark mode support for ui --- packages/dev/src/components/editor.tsx | 31 ++++++------ packages/tldraw/package.json | 4 +- .../page-options-dialog.tsx | 49 ++++++------------- .../src/components/page-panel/page-panel.tsx | 2 +- .../tldraw/src/components/tldraw/tldraw.tsx | 49 +++++++++++++++---- .../tldraw/src/hooks/useKeyboardShortcuts.tsx | 3 +- packages/tldraw/src/hooks/useTheme.ts | 24 +++++++-- .../tldraw/src/shape/shapes/draw/draw.tsx | 12 ++++- .../sessions/arrow/arrow.session.spec.ts | 11 ++--- packages/tldraw/src/state/tlstate.ts | 16 ++++-- packages/www/styles/stitches.config.ts | 4 +- yarn.lock | 8 +-- 12 files changed, 127 insertions(+), 86 deletions(-) diff --git a/packages/dev/src/components/editor.tsx b/packages/dev/src/components/editor.tsx index c283f90a3..c699c8e7a 100644 --- a/packages/dev/src/components/editor.tsx +++ b/packages/dev/src/components/editor.tsx @@ -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 - } + // const handleChange = React.useCallback( + // (tlstate: TLDrawState, patch: TLDrawPatch, reason: string) => { + // if (reason.startsWith('session')) { + // return + // } - setValue(tlstate.document) - }, - [setValue] - ) + // setValue(tlstate.document) + // }, + // [setValue] + // ) - if (status === 'loading' || value === null) { - return
- } + // if (status === 'loading' || value === null) { + // return
+ // } - return + // return + + // Will automatically persist data under the provided id, too + return } diff --git a/packages/tldraw/package.json b/packages/tldraw/package.json index f57e80e2e..950e01736 100644 --- a/packages/tldraw/package.json +++ b/packages/tldraw/package.json @@ -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" -} \ No newline at end of file +} diff --git a/packages/tldraw/src/components/page-options-dialog/page-options-dialog.tsx b/packages/tldraw/src/components/page-options-dialog/page-options-dialog.tsx index 78cd03999..91c6709c2 100644 --- a/packages/tldraw/src/components/page-options-dialog/page-options-dialog.tsx +++ b/packages/tldraw/src/components/page-options-dialog/page-options-dialog.tsx @@ -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(null) - const [name, setName] = React.useState(page.name || 'Page') - - const handleNameChange = React.useCallback((e: React.ChangeEvent) => { - setName(e.currentTarget.value) - }, []) - const handleDuplicate = React.useCallback(() => { tlstate.duplicatePage(page.id) + onClose?.() }, [tlstate]) const handleDelete = React.useCallback(() => { - tlstate.deletePage(page.id) + 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) { e.stopPropagation() } - function handleKeydown(e: React.KeyboardEvent) { - 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 - - - - + + Rename + Duplicate @@ -115,9 +97,6 @@ export function PageOptionsDialog({ page, onOpen }: PageOptionsDialogProps): JSX Delete - - Save - Cancel diff --git a/packages/tldraw/src/components/page-panel/page-panel.tsx b/packages/tldraw/src/components/page-panel/page-panel.tsx index 4eaac39fe..32f350140 100644 --- a/packages/tldraw/src/components/page-panel/page-panel.tsx +++ b/packages/tldraw/src/components/page-panel/page-panel.tsx @@ -100,7 +100,7 @@ function PageMenuContent({ onClose }: { onClose: () => void }) { - + ))} diff --git a/packages/tldraw/src/components/tldraw/tldraw.tsx b/packages/tldraw/src/components/tldraw/tldraw.tsx index 5cefe949c..f52d3cb74 100644 --- a/packages/tldraw/src/components/tldraw/tldraw.tsx +++ b/packages/tldraw/src/components/tldraw/tldraw.tsx @@ -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 diff --git a/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx b/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx index db1ce173a..b9649b3c1 100644 --- a/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx +++ b/packages/tldraw/src/hooks/useKeyboardShortcuts.tsx @@ -200,7 +200,8 @@ export function useKeyboardShortcuts(tlstate: TLDrawState) { tlstate.moveToFront() }) - useHotkeys('command+shift+backspace', () => { + useHotkeys('command+shift+backspace', (e) => { tlstate.reset() + e.preventDefault() }) } diff --git a/packages/tldraw/src/hooks/useTheme.ts b/packages/tldraw/src/hooks/useTheme.ts index 9667d184d..667a99fac 100644 --- a/packages/tldraw/src/hooks/useTheme.ts +++ b/packages/tldraw/src/hooks/useTheme.ts @@ -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, } } diff --git a/packages/tldraw/src/shape/shapes/draw/draw.tsx b/packages/tldraw/src/shape/shapes/draw/draw.tsx index 39ba2e884..f58e0147f 100644 --- a/packages/tldraw/src/shape/shapes/draw/draw.tsx +++ b/packages/tldraw/src/shape/shapes/draw/draw.tsx @@ -47,7 +47,9 @@ export class Draw extends TLDrawShapeUtil { // 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 { renderIndicator(shape: DrawShape): JSX.Element { const { points } = shape + const bounds = this.getBounds(shape) + + const verySmall = bounds.width < 4 && bounds.height < 4 + + if (verySmall) { + return + } + const path = Utils.getFromCache(this.simplePathCache, points, () => getSolidStrokePath(shape)) return diff --git a/packages/tldraw/src/state/session/sessions/arrow/arrow.session.spec.ts b/packages/tldraw/src/state/session/sessions/arrow/arrow.session.spec.ts index 62ed485c1..d8ce73242 100644 --- a/packages/tldraw/src/state/session/sessions/arrow/arrow.session.spec.ts +++ b/packages/tldraw/src/state/session/sessions/arrow/arrow.session.spec.ts @@ -157,17 +157,14 @@ describe('Arrow session', () => { expect(tlstate.getShape('arrow1').point).toStrictEqual([116, 116]) expect(tlstate.getShape('arrow1').handles.start.point).toStrictEqual([0, 0]) expect(tlstate.getShape('arrow1').handles.end.point).toStrictEqual([85, 85]) - // tlstate - // .select('target1') - // .startTranslateSession([50, 50]) - // .updateTranslateSession([300, 0]) - // .completeSession() - // expect(tlstate.getShape('arrow1').handles.start.point).toStrictEqual([66.493, 0]) - // expect(tlstate.getShape('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 + }) }) }) diff --git a/packages/tldraw/src/state/tlstate.ts b/packages/tldraw/src/state/tlstate.ts index 00cb2f71f..daf8ae074 100644 --- a/packages/tldraw/src/state/tlstate.ts +++ b/packages/tldraw/src/state/tlstate.ts @@ -91,8 +91,8 @@ const initialData: Data = { export class TLDrawState extends StateManager { _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 { } 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 => { diff --git a/packages/www/styles/stitches.config.ts b/packages/www/styles/stitches.config.ts index 9534c65d9..d48dfb840 100644 --- a/packages/www/styles/stitches.config.ts +++ b/packages/www/styles/stitches.config.ts @@ -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 } diff --git a/yarn.lock b/yarn.lock index 0e20f93f9..e06ba9f4d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"