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