diff --git a/apps/dotcom/client/public/tla/locales-compiled/en.json b/apps/dotcom/client/public/tla/locales-compiled/en.json index 5319dfe014e1..c0ad8e0b0713 100644 --- a/apps/dotcom/client/public/tla/locales-compiled/en.json +++ b/apps/dotcom/client/public/tla/locales-compiled/en.json @@ -219,6 +219,12 @@ "value": "Unsupported Mermaid diagram" } ], + "402bf0c47e": [ + { + "type": 0, + "value": "Color theme" + } + ], "42e53c47c1": [ { "type": 0, @@ -599,6 +605,12 @@ "value": "Submit an issue on GitHub" } ], + "7a1920d611": [ + { + "type": 0, + "value": "Default" + } + ], "7b1329f5ca": [ { "type": 0, diff --git a/apps/dotcom/client/public/tla/locales/en.json b/apps/dotcom/client/public/tla/locales/en.json index ee0b96b9306a..6196c92e2d45 100644 --- a/apps/dotcom/client/public/tla/locales/en.json +++ b/apps/dotcom/client/public/tla/locales/en.json @@ -107,6 +107,9 @@ "3e2ae04ff0": { "translation": "Unsupported Mermaid diagram" }, + "402bf0c47e": { + "translation": "Color theme" + }, "42e53c47c1": { "translation": "Contact the owner to request access." }, @@ -278,6 +281,9 @@ "797799f35e": { "translation": "Submit an issue on GitHub" }, + "7a1920d611": { + "translation": "Default" + }, "7b1329f5ca": { "translation": "User manual" }, diff --git a/apps/dotcom/client/src/tla/components/TlaButton/button.module.css b/apps/dotcom/client/src/tla/components/TlaButton/button.module.css index 9479402706ec..8131ef58496d 100644 --- a/apps/dotcom/client/src/tla/components/TlaButton/button.module.css +++ b/apps/dotcom/client/src/tla/components/TlaButton/button.module.css @@ -53,7 +53,7 @@ .cta { background-color: var(--tla-color-cta); - color: #ffffff; + color: var(--tla-color-contrast); font-weight: 600; } diff --git a/apps/dotcom/client/src/tla/components/TlaCtaButton/cta-button.module.css b/apps/dotcom/client/src/tla/components/TlaCtaButton/cta-button.module.css index bb9ea5a1d514..7d335597e680 100644 --- a/apps/dotcom/client/src/tla/components/TlaCtaButton/cta-button.module.css +++ b/apps/dotcom/client/src/tla/components/TlaCtaButton/cta-button.module.css @@ -14,7 +14,7 @@ padding: 0px 16px; font-size: 12px; font-weight: 600; - color: #ffffff; + color: var(--tla-color-contrast); border: none; cursor: pointer; } @@ -39,7 +39,7 @@ content: ''; position: absolute; inset: 0px; - color: #ffffff; + color: var(--tla-color-contrast); background-color: var(--tla-color-cta); border-radius: 6px; z-index: 2; diff --git a/apps/dotcom/client/src/tla/components/TlaEditor/TlaEditorTopLeftPanel.tsx b/apps/dotcom/client/src/tla/components/TlaEditor/TlaEditorTopLeftPanel.tsx index db0861c7b382..2ab7ce7470d2 100644 --- a/apps/dotcom/client/src/tla/components/TlaEditor/TlaEditorTopLeftPanel.tsx +++ b/apps/dotcom/client/src/tla/components/TlaEditor/TlaEditorTopLeftPanel.tsx @@ -2,13 +2,27 @@ import classNames from 'classnames' import { useCallback, useEffect, useRef, useState } from 'react' import { Link, useParams } from 'react-router-dom' import { + AccessibilityMenu, + ColorSchemeMenu, DefaultPageMenu, EditSubmenu, ExportFileContentSubMenu, ExtrasGroup, + InputModeMenu, + KeyboardShortcutsMenuItem, + LanguageMenu, PreferencesGroup, + ToggleDebugModeItem, + ToggleDynamicSizeModeItem, + ToggleEdgeScrollingItem, + ToggleFocusModeItem, + ToggleGridItem, + TogglePasteAtCursorItem, TldrawUiButton, TldrawUiButtonLabel, + ToggleSnapModeItem, + ToggleToolLockItem, + ToggleWrapModeItem, TldrawUiDropdownMenuContent, TldrawUiDropdownMenuRoot, TldrawUiDropdownMenuTrigger, @@ -35,6 +49,7 @@ import { GiveUsFeedbackMenuItem, LegalSummaryMenuItem, UserManualMenuItem, + UIThemeSubmenu, } from '../menu-items/menu-items' import { FileItems, TlaFileMenu } from '../TlaFileMenu/TlaFileMenu' import { TlaIcon } from '../TlaIcon/TlaIcon' @@ -138,7 +153,7 @@ export function TlaEditorTopLeftPanelAnonymous() { {canCopyToApp && } - + @@ -415,3 +430,33 @@ function SignInMenuItem() { ) } + +function TlaPreferencesGroup() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/apps/dotcom/client/src/tla/components/dialogs/TlaInviteDialog.module.css b/apps/dotcom/client/src/tla/components/dialogs/TlaInviteDialog.module.css index 87d85e771804..5b15daaef9b6 100644 --- a/apps/dotcom/client/src/tla/components/dialogs/TlaInviteDialog.module.css +++ b/apps/dotcom/client/src/tla/components/dialogs/TlaInviteDialog.module.css @@ -33,7 +33,7 @@ width: 100%; height: 32px; background-color: var(--tla-color-cta); - color: white; + color: var(--tla-color-contrast); border: none; border-radius: var(--tl-radius-1); font-weight: 500; diff --git a/apps/dotcom/client/src/tla/components/menu-items/menu-items.tsx b/apps/dotcom/client/src/tla/components/menu-items/menu-items.tsx index b794acb245c0..0075348131ec 100644 --- a/apps/dotcom/client/src/tla/components/menu-items/menu-items.tsx +++ b/apps/dotcom/client/src/tla/components/menu-items/menu-items.tsx @@ -50,6 +50,9 @@ const messages = defineMessages({ manageCookies: { defaultMessage: 'Manage cookies' }, about: { defaultMessage: 'About tldraw' }, submitFeedback: { defaultMessage: 'Send feedback' }, + // color theme + colorTheme: { defaultMessage: 'Color theme' }, + colorThemeDefault: { defaultMessage: 'Default' }, // debug appDebugFlags: { defaultMessage: 'App debug flags' }, langAccented: { defaultMessage: 'i18n: Accented' }, @@ -90,10 +93,9 @@ export function ColorThemeSubmenu() { return } -const THEME_NAMES: Record = { - default: 'Default', - ...Object.fromEntries(UI_THEMES.map(({ id, name }) => [id, name])), -} +const THEME_NAMES: Record = Object.fromEntries( + UI_THEMES.map(({ id, name }) => [id, name]) +) function UIThemeMenuCheckboxItem({ checked, @@ -140,6 +142,8 @@ export function UIThemeSubmenu() { const colorTheme = useValue('colorTheme', () => getLocalSessionState().colorTheme, []) const trackEvent = useTldrawAppUiEvents() const clearThemePreview = useCallback(() => setColorThemePreview(null), []) + const colorThemeLabel = useMsg(messages.colorTheme) + const defaultThemeLabel = useMsg(messages.colorThemeDefault) const themeIds = useValue('themeIds', () => (editor ? Object.keys(editor.getThemes()) : []), [ editor, @@ -150,7 +154,7 @@ export function UIThemeSubmenu() { if (!editor || themeIds.length === 0) return null return ( - +
( setColorThemePreview(id)} onSelect={() => { diff --git a/apps/dotcom/client/src/tla/themes/ui-themes.ts b/apps/dotcom/client/src/tla/themes/ui-themes.ts index 44ac9d0b31ab..542a93be70cd 100644 --- a/apps/dotcom/client/src/tla/themes/ui-themes.ts +++ b/apps/dotcom/client/src/tla/themes/ui-themes.ts @@ -284,7 +284,7 @@ const THEMES: UITheme[] = [ 'tla-color-text-1': '#f8f8f2', 'tla-color-text-2': '#e0e0dc', 'tla-color-text-3': '#6272a4', - 'tla-color-contrast': '#f8f8f2', + 'tla-color-contrast': '#ffffff', 'tla-color-low': '#343746', 'tla-color-border': '#565971', 'tla-color-hover-1': 'hsla(60, 30%, 96%, 0.03)', @@ -428,7 +428,7 @@ const THEMES: UITheme[] = [ 'tla-color-text-1': '#e5e9f0', 'tla-color-text-2': '#d8dee9', 'tla-color-text-3': '#7b88a1', - 'tla-color-contrast': '#eceff4', + 'tla-color-contrast': '#2e3440', 'tla-color-low': '#353c4a', 'tla-color-border': '#434c5e', 'tla-color-hover-1': 'hsla(218, 27%, 92%, 0.03)', @@ -474,7 +474,7 @@ const THEMES: UITheme[] = [ 'tl-color-low-border': '#e5e5e0', 'tl-color-divider': '#d6d6d0', 'tl-color-selected': '#66d9ef', - 'tl-color-selected-contrast': '#272822', + 'tl-color-selected-contrast': '#f7f7f7', 'tl-color-primary': '#66d9ef', 'tl-color-focus': '#66d9ef', 'tl-color-muted-none': 'hsla(60, 6%, 26%, 0)', @@ -483,7 +483,7 @@ const THEMES: UITheme[] = [ 'tl-color-muted-2': 'hsla(60, 6%, 26%, 0.043)', 'tl-color-hint': 'hsla(60, 6%, 26%, 0.055)', 'tl-color-overlay': 'hsla(60, 6%, 26%, 0.2)', - 'tl-color-tooltip': '#272822', + 'tl-color-tooltip': '#f7f7f7', 'tl-color-success': '#a6e22e', 'tl-color-info': '#66d9ef', 'tl-color-warning': '#fd971f', @@ -503,7 +503,7 @@ const THEMES: UITheme[] = [ 'tla-color-text-1': '#383830', 'tla-color-text-2': '#49483e', 'tla-color-text-3': '#8f908a', - 'tla-color-contrast': '#fafafa', + 'tla-color-contrast': '#f7f7f7', 'tla-color-low': '#f0f0ec', 'tla-color-border': '#d6d6d0', 'tla-color-hover-1': 'hsla(60, 6%, 26%, 0.03)', @@ -572,7 +572,7 @@ const THEMES: UITheme[] = [ 'tla-color-text-1': '#f8f8f2', 'tla-color-text-2': '#e6e6e0', 'tla-color-text-3': '#75715e', - 'tla-color-contrast': '#f8f8f2', + 'tla-color-contrast': '#272822', 'tla-color-low': '#2f302a', 'tla-color-border': '#49483e', 'tla-color-hover-1': 'hsla(60, 30%, 96%, 0.03)', @@ -860,7 +860,7 @@ const THEMES: UITheme[] = [ 'tla-color-text-1': '#cdd6f4', 'tla-color-text-2': '#bac2de', 'tla-color-text-3': '#6c7086', - 'tla-color-contrast': '#cdd6f4', + 'tla-color-contrast': '#1e1e2e', 'tla-color-low': '#28283a', 'tla-color-border': '#45475a', 'tla-color-hover-1': 'hsla(226, 64%, 88%, 0.03)', @@ -1004,7 +1004,7 @@ const THEMES: UITheme[] = [ 'tla-color-text-1': '#dcdfe4', 'tla-color-text-2': '#abb2bf', 'tla-color-text-3': '#5c6370', - 'tla-color-contrast': '#dcdfe4', + 'tla-color-contrast': '#282c34', 'tla-color-low': '#2c313a', 'tla-color-border': '#3e4451', 'tla-color-hover-1': 'hsla(219, 14%, 71%, 0.03)', @@ -1148,7 +1148,7 @@ const THEMES: UITheme[] = [ 'tla-color-text-1': '#c0caf5', 'tla-color-text-2': '#a9b1d6', 'tla-color-text-3': '#565f89', - 'tla-color-contrast': '#c0caf5', + 'tla-color-contrast': '#1a1b26', 'tla-color-low': '#1f2030', 'tla-color-border': '#2f3350', 'tla-color-hover-1': 'hsla(226, 33%, 75%, 0.03)', @@ -1292,7 +1292,7 @@ const THEMES: UITheme[] = [ 'tla-color-text-1': '#e0d5bb', 'tla-color-text-2': '#d3c6aa', 'tla-color-text-3': '#7a8478', - 'tla-color-contrast': '#e0d5bb', + 'tla-color-contrast': '#2d353b', 'tla-color-low': '#313b40', 'tla-color-border': '#3d484d', 'tla-color-hover-1': 'hsla(40, 28%, 73%, 0.03)', @@ -1436,7 +1436,7 @@ const THEMES: UITheme[] = [ 'tla-color-text-1': '#f0f6fc', 'tla-color-text-2': '#c9d1d9', 'tla-color-text-3': '#484f58', - 'tla-color-contrast': '#f0f6fc', + 'tla-color-contrast': '#0d1117', 'tla-color-low': '#12171e', 'tla-color-border': '#21262d', 'tla-color-hover-1': 'hsla(210, 29%, 80%, 0.03)', @@ -1580,7 +1580,7 @@ const THEMES: UITheme[] = [ 'tla-color-text-1': '#ececec', 'tla-color-text-2': '#cecdc3', 'tla-color-text-3': '#575653', - 'tla-color-contrast': '#ececec', + 'tla-color-contrast': '#fffcf0', 'tla-color-low': '#161514', 'tla-color-border': '#282726', 'tla-color-hover-1': 'hsla(40, 5%, 79%, 0.03)', @@ -1724,7 +1724,7 @@ const THEMES: UITheme[] = [ 'tla-color-text-1': '#d6deeb', 'tla-color-text-2': '#b0bec5', 'tla-color-text-3': '#5f7e97', - 'tla-color-contrast': '#d6deeb', + 'tla-color-contrast': '#011627', 'tla-color-low': '#071e31', 'tla-color-border': '#13344f', 'tla-color-hover-1': 'hsla(208, 44%, 84%, 0.03)', @@ -1868,7 +1868,7 @@ const THEMES: UITheme[] = [ 'tla-color-text-1': '#ffffff', 'tla-color-text-2': '#ffffff', 'tla-color-text-3': '#aaaaaa', - 'tla-color-contrast': '#ffffff', + 'tla-color-contrast': '#000000', 'tla-color-low': '#0a0a0a', 'tla-color-border': '#ffffff', 'tla-color-hover-1': 'hsla(0, 0%, 100%, 0.03)', diff --git a/packages/tldraw/api-report.api.md b/packages/tldraw/api-report.api.md index 12641ff4492a..94c46b70e5ba 100644 --- a/packages/tldraw/api-report.api.md +++ b/packages/tldraw/api-report.api.md @@ -2467,6 +2467,9 @@ export class ImageShapeUtil extends BaseBoxShapeUtil { export interface ImageShapeUtilDisplayValues { } +// @public (undocumented) +export function InputModeMenu(): JSX.Element; + // @public (undocumented) export const KeyboardShiftEnterTweakExtension: Extension; diff --git a/packages/tldraw/src/index.ts b/packages/tldraw/src/index.ts index 512c80e8bf6f..0d454a5895c4 100644 --- a/packages/tldraw/src/index.ts +++ b/packages/tldraw/src/index.ts @@ -351,6 +351,7 @@ export { } from './lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialog' export { DefaultKeyboardShortcutsDialogContent } from './lib/ui/components/KeyboardShortcutsDialog/DefaultKeyboardShortcutsDialogContent' export { LanguageMenu } from './lib/ui/components/LanguageMenu' +export { InputModeMenu } from './lib/ui/components/InputModeMenu' export { DefaultMainMenu, type TLUiMainMenuProps, diff --git a/packages/tldraw/src/lib/tools/SelectTool/childStates/EditingShape.ts b/packages/tldraw/src/lib/tools/SelectTool/childStates/EditingShape.ts index 7b83f4cfcaa7..7c6adc376175 100644 --- a/packages/tldraw/src/lib/tools/SelectTool/childStates/EditingShape.ts +++ b/packages/tldraw/src/lib/tools/SelectTool/childStates/EditingShape.ts @@ -44,6 +44,7 @@ export class EditingShape extends StateNode { this.parent.setCurrentToolIdMask('text') } + this.editor.setCursor({ type: 'default', rotation: 0 }) updateHoveredShapeId(this.editor) this.editor.select(editingShape) } diff --git a/packages/tldraw/src/lib/ui/hooks/useKeyboardShortcuts.ts b/packages/tldraw/src/lib/ui/hooks/useKeyboardShortcuts.ts index 85d94a351f45..6711625c4a34 100644 --- a/packages/tldraw/src/lib/ui/hooks/useKeyboardShortcuts.ts +++ b/packages/tldraw/src/lib/ui/hooks/useKeyboardShortcuts.ts @@ -118,11 +118,12 @@ export function useKeyboardShortcuts() { editor.inputs.keys.delete('Comma') - const { x, y, z } = editor.inputs.getCurrentScreenPoint() + const { x, y, z } = editor.inputs.getCurrentPagePoint() + const screenPoint = editor.pageToScreen({ x, y }) const info: TLPointerEventInfo = { type: 'pointer', name: 'pointer_up', - point: { x, y, z }, + point: { x: screenPoint.x, y: screenPoint.y, z }, shiftKey: e.shiftKey, altKey: e.altKey, ctrlKey: e.metaKey || e.ctrlKey, diff --git a/packages/tldraw/src/test/SelectTool.test.ts b/packages/tldraw/src/test/SelectTool.test.ts index cfd307edab49..6b299d071d07 100644 --- a/packages/tldraw/src/test/SelectTool.test.ts +++ b/packages/tldraw/src/test/SelectTool.test.ts @@ -443,6 +443,24 @@ describe('When double clicking the selection edge', () => { expect(editor.getEditingShapeId()).toBe(id) }) + + it('Resets the cursor to default when entering editing mode from a resize handle', () => { + const id = createShapeId() + editor + .selectAll() + .deleteShapes(editor.getSelectedShapeIds()) + .selectNone() + .createShapes([{ id, type: 'geo' }]) + .select(id) + + editor.setCursor({ type: 'ew-resize', rotation: 0 }) + expect(editor.getInstanceState().cursor.type).toBe('ew-resize') + + editor.doubleClick(100, 100, { target: 'selection', handle: 'left' }) + + expect(editor.getEditingShapeId()).toBe(id) + expect(editor.getInstanceState().cursor.type).toBe('default') + }) }) describe('When editing shapes', () => { diff --git a/packages/tldraw/src/test/commaKeyClick.test.ts b/packages/tldraw/src/test/commaKeyClick.test.ts new file mode 100644 index 000000000000..739f374149ca --- /dev/null +++ b/packages/tldraw/src/test/commaKeyClick.test.ts @@ -0,0 +1,94 @@ +import { TestEditor } from './TestEditor' + +let editor: TestEditor + +beforeEach(() => { + editor = new TestEditor() +}) + +/** + * Dispatches the pointer events that the comma key handler sends to the editor. + * The `useFixed` flag controls whether the pointer_up uses the fixed approach + * (pageToScreen, which produces absolute screen coords) or the old buggy approach + * (getCurrentScreenPoint, which is already canvas-relative and causes a double + * subtraction of screenBounds in updateFromEvent). + */ +function simulateCommaKeyClick(useFixed: boolean) { + const sharedProps = { + type: 'pointer' as const, + shiftKey: false, + altKey: false, + ctrlKey: false, + metaKey: false, + accelKey: false, + pointerId: 0, + button: 0, + isPen: false, + target: 'canvas' as const, + } + + // pointer_down: both old and new code use pageToScreen (correct absolute coords) + const { x: pdx, y: pdy, z: pdz } = editor.inputs.getCurrentPagePoint() + const screenPointDown = editor.pageToScreen({ x: pdx, y: pdy }) + editor.dispatch({ + ...sharedProps, + name: 'pointer_down', + point: { x: screenPointDown.x, y: screenPointDown.y, z: pdz }, + }) + + if (useFixed) { + // Fixed pointer_up: also use pageToScreen (absolute coords) + const { x: pux, y: puy, z: puz } = editor.inputs.getCurrentPagePoint() + const screenPointUp = editor.pageToScreen({ x: pux, y: puy }) + editor.dispatch({ + ...sharedProps, + name: 'pointer_up', + point: { x: screenPointUp.x, y: screenPointUp.y, z: puz }, + }) + } else { + // Buggy pointer_up: use getCurrentScreenPoint (canvas-relative — causes double subtraction) + const { x, y, z } = editor.inputs.getCurrentScreenPoint() + editor.dispatch({ ...sharedProps, name: 'pointer_up', point: { x, y, z } }) + } +} + +describe('comma key click - screenBounds offset', () => { + it('page point is unchanged after a click when screenBounds has no offset', () => { + // Default screenBounds has x=0, so both old and new code agree + editor.pointerMove(200, 200) + const before = { ...editor.inputs.getCurrentPagePoint() } + + simulateCommaKeyClick(true) + + const after = editor.inputs.getCurrentPagePoint() + expect(after.x).toBeCloseTo(before.x) + expect(after.y).toBeCloseTo(before.y) + }) + + it('page point is unchanged after a click when screenBounds has a non-zero x offset (fixed)', () => { + // Simulate a host that offsets the canvas by 300px (e.g. a wide sidebar) + editor.setScreenBounds({ x: 300, y: 0, w: 800, h: 600 }) + editor.pointerMove(500, 200) // absolute screen x=500, canvas-relative x=200 + const before = { ...editor.inputs.getCurrentPagePoint() } + + simulateCommaKeyClick(true) + + const after = editor.inputs.getCurrentPagePoint() + expect(after.x).toBeCloseTo(before.x) + expect(after.y).toBeCloseTo(before.y) + }) + + it('page point is wrong after a click when screenBounds has a non-zero x offset (regression — old buggy approach)', () => { + const screenBoundsX = 300 + editor.setScreenBounds({ x: screenBoundsX, y: 0, w: 800, h: 600 }) + editor.pointerMove(500, 200) + const before = { ...editor.inputs.getCurrentPagePoint() } + + simulateCommaKeyClick(false) // old buggy approach + + // The bug causes pointer_up to land screenBoundsX / camera.z to the left + const after = editor.inputs.getCurrentPagePoint() + const cameraZ = editor.getCamera().z + expect(after.x).toBeCloseTo(before.x - screenBoundsX / cameraZ) + }) +})