diff --git a/apps/examples/e2e/tests/export-snapshots.spec.ts b/apps/examples/e2e/tests/export-snapshots.spec.ts index 5246f5e23..6ef2ca390 100644 --- a/apps/examples/e2e/tests/export-snapshots.spec.ts +++ b/apps/examples/e2e/tests/export-snapshots.spec.ts @@ -208,7 +208,7 @@ test.describe('Export snapshots', () => { for (const [name, shapes] of filteredSnapshots) { test(`Exports with ${name} in dark mode`, async ({ page, api }) => { await page.evaluate((shapes) => { - editor.user.updateUserPreferences({ isDarkMode: true }) + editor.user.updateUserPreferences({ colorScheme: 'dark' }) editor .updateInstanceState({ exportBackground: false }) .selectAll() diff --git a/apps/examples/e2e/tests/test-kbds.spec.ts b/apps/examples/e2e/tests/test-kbds.spec.ts index 3b78b7f12..addc039be 100644 --- a/apps/examples/e2e/tests/test-kbds.spec.ts +++ b/apps/examples/e2e/tests/test-kbds.spec.ts @@ -187,7 +187,7 @@ test.describe('Keyboard Shortcuts', () => { test('Toggle dark mode', async () => { await page.keyboard.press('Control+/') expect(await page.evaluate(() => __tldraw_ui_event)).toMatchObject({ - name: 'toggle-dark-mode', + name: 'color-scheme', data: { source: 'kbd' }, }) }) diff --git a/apps/examples/src/examples/image-component/TldrawImageExample.tsx b/apps/examples/src/examples/image-component/TldrawImageExample.tsx index 6d5d6e8b3..4720fb186 100644 --- a/apps/examples/src/examples/image-component/TldrawImageExample.tsx +++ b/apps/examples/src/examples/image-component/TldrawImageExample.tsx @@ -73,7 +73,7 @@ export default function TldrawImageExample() { onMount={(editor: Editor) => { setEditor(editor) editor.updateInstanceState({ isDebugMode: false }) - editor.user.updateUserPreferences({ isDarkMode }) + editor.user.updateUserPreferences({ colorScheme: isDarkMode ? 'dark' : 'light' }) if (currentPageId) { editor.setCurrentPage(currentPageId) } diff --git a/assets/translations/main.json b/assets/translations/main.json index 0ba22b989..f5686d7b3 100644 --- a/assets/translations/main.json +++ b/assets/translations/main.json @@ -115,6 +115,9 @@ "action.zoom-to-selection": "Zoom to selection", "assets.files.upload-failed": "Upload failed", "assets.url.failed": "Couldn't load URL preview", + "color-scheme.dark": "Dark", + "color-scheme.light": "Light", + "color-scheme.system": "System", "color-style.white": "White", "color-style.black": "Black", "color-style.blue": "Blue", @@ -228,6 +231,7 @@ "tool.embed": "Embed", "tool.text": "Text", "menu.title": "Menu", + "menu.color-scheme": "Color scheme", "menu.copy-as": "Copy as", "menu.edit": "Edit", "menu.export-as": "Export as", diff --git a/packages/editor/api-report.md b/packages/editor/api-report.md index 9c9efeef1..fd91dcc13 100644 --- a/packages/editor/api-report.md +++ b/packages/editor/api-report.md @@ -718,7 +718,6 @@ export const defaultUserPreferences: Readonly<{ animationSpeed: 0 | 1; color: "#02B1CC" | "#11B3A3" | "#39B178" | "#55B467" | "#7B66DC" | "#9D5BD2" | "#BD54C6" | "#E34BA9" | "#EC5E41" | "#F04F88" | "#F2555A" | "#FF802B"; edgeScrollSpeed: 1; - isDarkMode: false; isDynamicSizeMode: false; isSnapMode: false; isWrapMode: false; @@ -3343,12 +3342,12 @@ export interface TLUserPreferences { // (undocumented) color?: null | string; // (undocumented) + colorScheme?: 'dark' | 'light' | 'system'; + // (undocumented) edgeScrollSpeed?: null | number; // (undocumented) id: string; // (undocumented) - isDarkMode?: boolean | null; - // (undocumented) isDynamicSizeMode?: boolean | null; // (undocumented) isSnapMode?: boolean | null; @@ -3470,6 +3469,7 @@ export class UserPreferencesManager { getUserPreferences(): { animationSpeed: number; color: string; + colorScheme: "dark" | "light" | "system" | undefined; id: string; isDarkMode: boolean; isDynamicResizeMode: boolean; @@ -3479,6 +3479,8 @@ export class UserPreferencesManager { name: string; }; // (undocumented) + systemColorScheme: Atom<"dark" | "light", unknown>; + // (undocumented) updateUserPreferences: (userPreferences: Partial) => void; } diff --git a/packages/editor/src/lib/TldrawEditor.tsx b/packages/editor/src/lib/TldrawEditor.tsx index 1d7dce676..1d49cb4f9 100644 --- a/packages/editor/src/lib/TldrawEditor.tsx +++ b/packages/editor/src/lib/TldrawEditor.tsx @@ -261,7 +261,7 @@ const TldrawEditorWithLoadingStore = memo(function TldrawEditorBeforeLoading({ const container = useContainer() useLayoutEffect(() => { - if (user.userPreferences.get().isDarkMode) { + if (user.userPreferences.get().colorScheme === 'dark') { container.classList.remove('tl-theme__light') container.classList.add('tl-theme__dark') } diff --git a/packages/editor/src/lib/config/TLUserPreferences.ts b/packages/editor/src/lib/config/TLUserPreferences.ts index 3cc44407e..614cc6929 100644 --- a/packages/editor/src/lib/config/TLUserPreferences.ts +++ b/packages/editor/src/lib/config/TLUserPreferences.ts @@ -18,7 +18,7 @@ export interface TLUserPreferences { color?: string | null animationSpeed?: number | null edgeScrollSpeed?: number | null - isDarkMode?: boolean | null + colorScheme?: 'light' | 'dark' | 'system' isSnapMode?: boolean | null isWrapMode?: boolean | null isDynamicSizeMode?: boolean | null @@ -40,9 +40,9 @@ const userTypeValidator: T.Validator = T.object('systemColorScheme', 'light') constructor( private readonly user: TLUser, private readonly inferDarkMode: boolean - ) {} + ) { + if (window) { + const darkModeMediaQuery = window.matchMedia?.('(prefers-color-scheme: dark)') + if (darkModeMediaQuery?.matches) { + this.systemColorScheme.set('dark') + } + darkModeMediaQuery?.addEventListener('change', (e) => { + if (e.matches) { + this.systemColorScheme.set('dark') + } else { + this.systemColorScheme.set('light') + } + }) + } + } updateUserPreferences = (userPreferences: Partial) => { this.user.setUserPreferences({ @@ -27,16 +38,23 @@ export class UserPreferencesManager { color: this.getColor(), animationSpeed: this.getAnimationSpeed(), isSnapMode: this.getIsSnapMode(), + colorScheme: this.user.userPreferences.get().colorScheme, isDarkMode: this.getIsDarkMode(), isWrapMode: this.getIsWrapMode(), isDynamicResizeMode: this.getIsDynamicResizeMode(), } } @computed getIsDarkMode() { - return ( - this.user.userPreferences.get().isDarkMode ?? - (this.inferDarkMode ? userPrefersDarkUI() : false) - ) + switch (this.user.userPreferences.get().colorScheme) { + case 'dark': + return true + case 'light': + return false + case 'system': + return this.systemColorScheme.get() === 'dark' + default: + return this.inferDarkMode ? this.systemColorScheme.get() === 'dark' : false + } } /** diff --git a/packages/editor/src/lib/test/user.test.ts b/packages/editor/src/lib/test/user.test.ts index 09c46d861..18b6d23db 100644 --- a/packages/editor/src/lib/test/user.test.ts +++ b/packages/editor/src/lib/test/user.test.ts @@ -19,7 +19,7 @@ describe('user', () => { }) it('gets a user with the correct', () => { - editor.user.updateUserPreferences({ isDarkMode: true }) + editor.user.updateUserPreferences({ colorScheme: 'dark' }) expect(editor.user.getIsDarkMode()).toBe(true) }) }) diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md index 680c612de..be13525c5 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -2193,6 +2193,10 @@ export interface TLUiEventMap { id: string; }; // (undocumented) + 'color-scheme': { + value: string; + }; + // (undocumented) 'convert-to-bookmark': null; // (undocumented) 'convert-to-embed': null; @@ -2292,8 +2296,6 @@ export interface TLUiEventMap { // (undocumented) 'toggle-auto-size': null; // (undocumented) - 'toggle-dark-mode': null; - // (undocumented) 'toggle-debug-mode': null; // (undocumented) 'toggle-dynamic-size-mode': null; @@ -2692,7 +2694,7 @@ export interface TLUiTranslation { export type TLUiTranslationContextType = TLUiTranslation; // @public (undocumented) -export type TLUiTranslationKey = 'action.align-bottom' | 'action.align-center-horizontal.short' | 'action.align-center-horizontal' | 'action.align-center-vertical.short' | 'action.align-center-vertical' | 'action.align-left' | 'action.align-right' | 'action.align-top' | 'action.back-to-content' | 'action.bring-forward' | 'action.bring-to-front' | 'action.convert-to-bookmark' | 'action.convert-to-embed' | 'action.copy-as-json.short' | 'action.copy-as-json' | 'action.copy-as-png.short' | 'action.copy-as-png' | 'action.copy-as-svg.short' | 'action.copy-as-svg' | 'action.copy' | 'action.cut' | 'action.delete' | 'action.distribute-horizontal.short' | 'action.distribute-horizontal' | 'action.distribute-vertical.short' | 'action.distribute-vertical' | 'action.duplicate' | 'action.edit-link' | 'action.exit-pen-mode' | 'action.export-all-as-json.short' | 'action.export-all-as-json' | 'action.export-all-as-png.short' | 'action.export-all-as-png' | 'action.export-all-as-svg.short' | 'action.export-all-as-svg' | 'action.export-as-json.short' | 'action.export-as-json' | 'action.export-as-png.short' | 'action.export-as-png' | 'action.export-as-svg.short' | 'action.export-as-svg' | 'action.fit-frame-to-content' | 'action.flatten-to-image' | 'action.flip-horizontal.short' | 'action.flip-horizontal' | 'action.flip-vertical.short' | 'action.flip-vertical' | 'action.fork-project-on-tldraw' | 'action.fork-project' | 'action.group' | 'action.insert-embed' | 'action.insert-media' | 'action.leave-shared-project' | 'action.new-project' | 'action.new-shared-project' | 'action.open-cursor-chat' | 'action.open-embed-link' | 'action.open-file' | 'action.pack' | 'action.paste' | 'action.print' | 'action.redo' | 'action.remove-frame' | 'action.rename' | 'action.rotate-ccw' | 'action.rotate-cw' | 'action.save-copy' | 'action.select-all' | 'action.select-none' | 'action.send-backward' | 'action.send-to-back' | 'action.share-project' | 'action.stack-horizontal.short' | 'action.stack-horizontal' | 'action.stack-vertical.short' | 'action.stack-vertical' | 'action.stop-following' | 'action.stretch-horizontal.short' | 'action.stretch-horizontal' | 'action.stretch-vertical.short' | 'action.stretch-vertical' | 'action.toggle-auto-size' | 'action.toggle-dark-mode.menu' | 'action.toggle-dark-mode' | 'action.toggle-debug-mode.menu' | 'action.toggle-debug-mode' | 'action.toggle-dynamic-size-mode.menu' | 'action.toggle-dynamic-size-mode' | 'action.toggle-edge-scrolling.menu' | 'action.toggle-edge-scrolling' | 'action.toggle-focus-mode.menu' | 'action.toggle-focus-mode' | 'action.toggle-grid.menu' | 'action.toggle-grid' | 'action.toggle-lock' | 'action.toggle-reduce-motion.menu' | 'action.toggle-reduce-motion' | 'action.toggle-snap-mode.menu' | 'action.toggle-snap-mode' | 'action.toggle-tool-lock.menu' | 'action.toggle-tool-lock' | 'action.toggle-transparent.context-menu' | 'action.toggle-transparent.menu' | 'action.toggle-transparent' | 'action.toggle-wrap-mode.menu' | 'action.toggle-wrap-mode' | 'action.undo' | 'action.ungroup' | 'action.unlock-all' | 'action.zoom-in' | 'action.zoom-out' | 'action.zoom-to-100' | 'action.zoom-to-fit' | 'action.zoom-to-selection' | 'actions-menu.title' | 'align-style.end' | 'align-style.justify' | 'align-style.middle' | 'align-style.start' | 'arrowheadEnd-style.arrow' | 'arrowheadEnd-style.bar' | 'arrowheadEnd-style.diamond' | 'arrowheadEnd-style.dot' | 'arrowheadEnd-style.inverted' | 'arrowheadEnd-style.none' | 'arrowheadEnd-style.pipe' | 'arrowheadEnd-style.square' | 'arrowheadEnd-style.triangle' | 'arrowheadStart-style.arrow' | 'arrowheadStart-style.bar' | 'arrowheadStart-style.diamond' | 'arrowheadStart-style.dot' | 'arrowheadStart-style.inverted' | 'arrowheadStart-style.none' | 'arrowheadStart-style.pipe' | 'arrowheadStart-style.square' | 'arrowheadStart-style.triangle' | 'assets.files.upload-failed' | 'assets.url.failed' | 'color-style.black' | 'color-style.blue' | 'color-style.green' | 'color-style.grey' | 'color-style.light-blue' | 'color-style.light-green' | 'color-style.light-red' | 'color-style.light-violet' | 'color-style.orange' | 'color-style.red' | 'color-style.violet' | 'color-style.white' | 'color-style.yellow' | 'context-menu.arrange' | 'context-menu.copy-as' | 'context-menu.edit' | 'context-menu.export-all-as' | 'context-menu.export-as' | 'context-menu.move-to-page' | 'context-menu.reorder' | 'context.pages.new-page' | 'cursor-chat.type-to-chat' | 'dash-style.dashed' | 'dash-style.dotted' | 'dash-style.draw' | 'dash-style.solid' | 'debug-panel.more' | 'document.default-name' | 'edit-link-dialog.cancel' | 'edit-link-dialog.clear' | 'edit-link-dialog.detail' | 'edit-link-dialog.invalid-url' | 'edit-link-dialog.save' | 'edit-link-dialog.title' | 'edit-link-dialog.url' | 'edit-pages-dialog.move-down' | 'edit-pages-dialog.move-up' | 'embed-dialog.back' | 'embed-dialog.cancel' | 'embed-dialog.create' | 'embed-dialog.instruction' | 'embed-dialog.invalid-url' | 'embed-dialog.title' | 'embed-dialog.url' | 'file-system.confirm-clear.cancel' | 'file-system.confirm-clear.continue' | 'file-system.confirm-clear.description' | 'file-system.confirm-clear.dont-show-again' | 'file-system.confirm-clear.title' | 'file-system.confirm-open.cancel' | 'file-system.confirm-open.description' | 'file-system.confirm-open.dont-show-again' | 'file-system.confirm-open.open' | 'file-system.confirm-open.title' | 'file-system.file-open-error.file-format-version-too-new' | 'file-system.file-open-error.generic-corrupted-file' | 'file-system.file-open-error.not-a-tldraw-file' | 'file-system.file-open-error.title' | 'file-system.shared-document-file-open-error.description' | 'file-system.shared-document-file-open-error.title' | 'fill-style.none' | 'fill-style.pattern' | 'fill-style.semi' | 'fill-style.solid' | 'focus-mode.toggle-focus-mode' | 'font-style.draw' | 'font-style.mono' | 'font-style.sans' | 'font-style.serif' | 'geo-style.arrow-down' | 'geo-style.arrow-left' | 'geo-style.arrow-right' | 'geo-style.arrow-up' | 'geo-style.check-box' | 'geo-style.cloud' | 'geo-style.diamond' | 'geo-style.ellipse' | 'geo-style.hexagon' | 'geo-style.octagon' | 'geo-style.oval' | 'geo-style.pentagon' | 'geo-style.rectangle' | 'geo-style.rhombus-2' | 'geo-style.rhombus' | 'geo-style.star' | 'geo-style.trapezoid' | 'geo-style.triangle' | 'geo-style.x-box' | 'help-menu.about' | 'help-menu.discord' | 'help-menu.github' | 'help-menu.keyboard-shortcuts' | 'help-menu.title' | 'help-menu.twitter' | 'home-project-dialog.description' | 'home-project-dialog.ok' | 'home-project-dialog.title' | 'menu.copy-as' | 'menu.edit' | 'menu.export-as' | 'menu.file' | 'menu.language' | 'menu.preferences' | 'menu.title' | 'menu.view' | 'navigation-zone.toggle-minimap' | 'navigation-zone.zoom' | 'opacity-style.0.1' | 'opacity-style.0.25' | 'opacity-style.0.5' | 'opacity-style.0.75' | 'opacity-style.1' | 'page-menu.create-new-page' | 'page-menu.edit-done' | 'page-menu.edit-start' | 'page-menu.go-to-page' | 'page-menu.max-page-count-reached' | 'page-menu.new-page-initial-name' | 'page-menu.submenu.delete' | 'page-menu.submenu.duplicate-page' | 'page-menu.submenu.move-down' | 'page-menu.submenu.move-up' | 'page-menu.submenu.rename' | 'page-menu.submenu.title' | 'page-menu.title' | 'people-menu.change-color' | 'people-menu.change-name' | 'people-menu.follow' | 'people-menu.following' | 'people-menu.invite' | 'people-menu.leading' | 'people-menu.title' | 'people-menu.user' | 'rename-project-dialog.cancel' | 'rename-project-dialog.rename' | 'rename-project-dialog.title' | 'share-menu.copied' | 'share-menu.copy-link-note' | 'share-menu.copy-link' | 'share-menu.copy-readonly-link-note' | 'share-menu.copy-readonly-link' | 'share-menu.create-snapshot-link' | 'share-menu.creating-project' | 'share-menu.default-project-name' | 'share-menu.fork-note' | 'share-menu.offline-note' | 'share-menu.project-too-large' | 'share-menu.readonly-link' | 'share-menu.save-note' | 'share-menu.share-project' | 'share-menu.snapshot-link-note' | 'share-menu.title' | 'share-menu.upload-failed' | 'sharing.confirm-leave.cancel' | 'sharing.confirm-leave.description' | 'sharing.confirm-leave.dont-show-again' | 'sharing.confirm-leave.leave' | 'sharing.confirm-leave.title' | 'shortcuts-dialog.collaboration' | 'shortcuts-dialog.edit' | 'shortcuts-dialog.file' | 'shortcuts-dialog.preferences' | 'shortcuts-dialog.title' | 'shortcuts-dialog.tools' | 'shortcuts-dialog.transform' | 'shortcuts-dialog.view' | 'size-style.l' | 'size-style.m' | 'size-style.s' | 'size-style.xl' | 'spline-style.cubic' | 'spline-style.line' | 'status.offline' | 'status.online' | 'style-panel.align' | 'style-panel.arrowhead-end' | 'style-panel.arrowhead-start' | 'style-panel.arrowheads' | 'style-panel.color' | 'style-panel.dash' | 'style-panel.fill' | 'style-panel.font' | 'style-panel.geo' | 'style-panel.mixed' | 'style-panel.opacity' | 'style-panel.position' | 'style-panel.size' | 'style-panel.spline' | 'style-panel.title' | 'style-panel.vertical-align' | 'toast.close' | 'toast.error.copy-fail.desc' | 'toast.error.copy-fail.title' | 'toast.error.export-fail.desc' | 'toast.error.export-fail.title' | 'tool-panel.drawing' | 'tool-panel.more' | 'tool-panel.shapes' | 'tool.arrow-down' | 'tool.arrow-left' | 'tool.arrow-right' | 'tool.arrow-up' | 'tool.arrow' | 'tool.asset' | 'tool.check-box' | 'tool.cloud' | 'tool.diamond' | 'tool.draw' | 'tool.ellipse' | 'tool.embed' | 'tool.eraser' | 'tool.frame' | 'tool.hand' | 'tool.hexagon' | 'tool.highlight' | 'tool.laser' | 'tool.line' | 'tool.note' | 'tool.octagon' | 'tool.oval' | 'tool.pentagon' | 'tool.rectangle' | 'tool.rhombus' | 'tool.select' | 'tool.star' | 'tool.text' | 'tool.trapezoid' | 'tool.triangle' | 'tool.x-box' | 'verticalAlign-style.end' | 'verticalAlign-style.middle' | 'verticalAlign-style.start' | 'vscode.file-open.backup-failed' | 'vscode.file-open.backup-saved' | 'vscode.file-open.backup' | 'vscode.file-open.desc' | 'vscode.file-open.dont-show-again' | 'vscode.file-open.open'; +export type TLUiTranslationKey = 'action.align-bottom' | 'action.align-center-horizontal.short' | 'action.align-center-horizontal' | 'action.align-center-vertical.short' | 'action.align-center-vertical' | 'action.align-left' | 'action.align-right' | 'action.align-top' | 'action.back-to-content' | 'action.bring-forward' | 'action.bring-to-front' | 'action.convert-to-bookmark' | 'action.convert-to-embed' | 'action.copy-as-json.short' | 'action.copy-as-json' | 'action.copy-as-png.short' | 'action.copy-as-png' | 'action.copy-as-svg.short' | 'action.copy-as-svg' | 'action.copy' | 'action.cut' | 'action.delete' | 'action.distribute-horizontal.short' | 'action.distribute-horizontal' | 'action.distribute-vertical.short' | 'action.distribute-vertical' | 'action.duplicate' | 'action.edit-link' | 'action.exit-pen-mode' | 'action.export-all-as-json.short' | 'action.export-all-as-json' | 'action.export-all-as-png.short' | 'action.export-all-as-png' | 'action.export-all-as-svg.short' | 'action.export-all-as-svg' | 'action.export-as-json.short' | 'action.export-as-json' | 'action.export-as-png.short' | 'action.export-as-png' | 'action.export-as-svg.short' | 'action.export-as-svg' | 'action.fit-frame-to-content' | 'action.flatten-to-image' | 'action.flip-horizontal.short' | 'action.flip-horizontal' | 'action.flip-vertical.short' | 'action.flip-vertical' | 'action.fork-project-on-tldraw' | 'action.fork-project' | 'action.group' | 'action.insert-embed' | 'action.insert-media' | 'action.leave-shared-project' | 'action.new-project' | 'action.new-shared-project' | 'action.open-cursor-chat' | 'action.open-embed-link' | 'action.open-file' | 'action.pack' | 'action.paste' | 'action.print' | 'action.redo' | 'action.remove-frame' | 'action.rename' | 'action.rotate-ccw' | 'action.rotate-cw' | 'action.save-copy' | 'action.select-all' | 'action.select-none' | 'action.send-backward' | 'action.send-to-back' | 'action.share-project' | 'action.stack-horizontal.short' | 'action.stack-horizontal' | 'action.stack-vertical.short' | 'action.stack-vertical' | 'action.stop-following' | 'action.stretch-horizontal.short' | 'action.stretch-horizontal' | 'action.stretch-vertical.short' | 'action.stretch-vertical' | 'action.toggle-auto-size' | 'action.toggle-dark-mode.menu' | 'action.toggle-dark-mode' | 'action.toggle-debug-mode.menu' | 'action.toggle-debug-mode' | 'action.toggle-dynamic-size-mode.menu' | 'action.toggle-dynamic-size-mode' | 'action.toggle-edge-scrolling.menu' | 'action.toggle-edge-scrolling' | 'action.toggle-focus-mode.menu' | 'action.toggle-focus-mode' | 'action.toggle-grid.menu' | 'action.toggle-grid' | 'action.toggle-lock' | 'action.toggle-reduce-motion.menu' | 'action.toggle-reduce-motion' | 'action.toggle-snap-mode.menu' | 'action.toggle-snap-mode' | 'action.toggle-tool-lock.menu' | 'action.toggle-tool-lock' | 'action.toggle-transparent.context-menu' | 'action.toggle-transparent.menu' | 'action.toggle-transparent' | 'action.toggle-wrap-mode.menu' | 'action.toggle-wrap-mode' | 'action.undo' | 'action.ungroup' | 'action.unlock-all' | 'action.zoom-in' | 'action.zoom-out' | 'action.zoom-to-100' | 'action.zoom-to-fit' | 'action.zoom-to-selection' | 'actions-menu.title' | 'align-style.end' | 'align-style.justify' | 'align-style.middle' | 'align-style.start' | 'arrowheadEnd-style.arrow' | 'arrowheadEnd-style.bar' | 'arrowheadEnd-style.diamond' | 'arrowheadEnd-style.dot' | 'arrowheadEnd-style.inverted' | 'arrowheadEnd-style.none' | 'arrowheadEnd-style.pipe' | 'arrowheadEnd-style.square' | 'arrowheadEnd-style.triangle' | 'arrowheadStart-style.arrow' | 'arrowheadStart-style.bar' | 'arrowheadStart-style.diamond' | 'arrowheadStart-style.dot' | 'arrowheadStart-style.inverted' | 'arrowheadStart-style.none' | 'arrowheadStart-style.pipe' | 'arrowheadStart-style.square' | 'arrowheadStart-style.triangle' | 'assets.files.upload-failed' | 'assets.url.failed' | 'color-scheme.dark' | 'color-scheme.light' | 'color-scheme.system' | 'color-style.black' | 'color-style.blue' | 'color-style.green' | 'color-style.grey' | 'color-style.light-blue' | 'color-style.light-green' | 'color-style.light-red' | 'color-style.light-violet' | 'color-style.orange' | 'color-style.red' | 'color-style.violet' | 'color-style.white' | 'color-style.yellow' | 'context-menu.arrange' | 'context-menu.copy-as' | 'context-menu.edit' | 'context-menu.export-all-as' | 'context-menu.export-as' | 'context-menu.move-to-page' | 'context-menu.reorder' | 'context.pages.new-page' | 'cursor-chat.type-to-chat' | 'dash-style.dashed' | 'dash-style.dotted' | 'dash-style.draw' | 'dash-style.solid' | 'debug-panel.more' | 'document.default-name' | 'edit-link-dialog.cancel' | 'edit-link-dialog.clear' | 'edit-link-dialog.detail' | 'edit-link-dialog.invalid-url' | 'edit-link-dialog.save' | 'edit-link-dialog.title' | 'edit-link-dialog.url' | 'edit-pages-dialog.move-down' | 'edit-pages-dialog.move-up' | 'embed-dialog.back' | 'embed-dialog.cancel' | 'embed-dialog.create' | 'embed-dialog.instruction' | 'embed-dialog.invalid-url' | 'embed-dialog.title' | 'embed-dialog.url' | 'file-system.confirm-clear.cancel' | 'file-system.confirm-clear.continue' | 'file-system.confirm-clear.description' | 'file-system.confirm-clear.dont-show-again' | 'file-system.confirm-clear.title' | 'file-system.confirm-open.cancel' | 'file-system.confirm-open.description' | 'file-system.confirm-open.dont-show-again' | 'file-system.confirm-open.open' | 'file-system.confirm-open.title' | 'file-system.file-open-error.file-format-version-too-new' | 'file-system.file-open-error.generic-corrupted-file' | 'file-system.file-open-error.not-a-tldraw-file' | 'file-system.file-open-error.title' | 'file-system.shared-document-file-open-error.description' | 'file-system.shared-document-file-open-error.title' | 'fill-style.none' | 'fill-style.pattern' | 'fill-style.semi' | 'fill-style.solid' | 'focus-mode.toggle-focus-mode' | 'font-style.draw' | 'font-style.mono' | 'font-style.sans' | 'font-style.serif' | 'geo-style.arrow-down' | 'geo-style.arrow-left' | 'geo-style.arrow-right' | 'geo-style.arrow-up' | 'geo-style.check-box' | 'geo-style.cloud' | 'geo-style.diamond' | 'geo-style.ellipse' | 'geo-style.hexagon' | 'geo-style.octagon' | 'geo-style.oval' | 'geo-style.pentagon' | 'geo-style.rectangle' | 'geo-style.rhombus-2' | 'geo-style.rhombus' | 'geo-style.star' | 'geo-style.trapezoid' | 'geo-style.triangle' | 'geo-style.x-box' | 'help-menu.about' | 'help-menu.discord' | 'help-menu.github' | 'help-menu.keyboard-shortcuts' | 'help-menu.title' | 'help-menu.twitter' | 'home-project-dialog.description' | 'home-project-dialog.ok' | 'home-project-dialog.title' | 'menu.color-scheme' | 'menu.copy-as' | 'menu.edit' | 'menu.export-as' | 'menu.file' | 'menu.language' | 'menu.preferences' | 'menu.title' | 'menu.view' | 'navigation-zone.toggle-minimap' | 'navigation-zone.zoom' | 'opacity-style.0.1' | 'opacity-style.0.25' | 'opacity-style.0.5' | 'opacity-style.0.75' | 'opacity-style.1' | 'page-menu.create-new-page' | 'page-menu.edit-done' | 'page-menu.edit-start' | 'page-menu.go-to-page' | 'page-menu.max-page-count-reached' | 'page-menu.new-page-initial-name' | 'page-menu.submenu.delete' | 'page-menu.submenu.duplicate-page' | 'page-menu.submenu.move-down' | 'page-menu.submenu.move-up' | 'page-menu.submenu.rename' | 'page-menu.submenu.title' | 'page-menu.title' | 'people-menu.change-color' | 'people-menu.change-name' | 'people-menu.follow' | 'people-menu.following' | 'people-menu.invite' | 'people-menu.leading' | 'people-menu.title' | 'people-menu.user' | 'rename-project-dialog.cancel' | 'rename-project-dialog.rename' | 'rename-project-dialog.title' | 'share-menu.copied' | 'share-menu.copy-link-note' | 'share-menu.copy-link' | 'share-menu.copy-readonly-link-note' | 'share-menu.copy-readonly-link' | 'share-menu.create-snapshot-link' | 'share-menu.creating-project' | 'share-menu.default-project-name' | 'share-menu.fork-note' | 'share-menu.offline-note' | 'share-menu.project-too-large' | 'share-menu.readonly-link' | 'share-menu.save-note' | 'share-menu.share-project' | 'share-menu.snapshot-link-note' | 'share-menu.title' | 'share-menu.upload-failed' | 'sharing.confirm-leave.cancel' | 'sharing.confirm-leave.description' | 'sharing.confirm-leave.dont-show-again' | 'sharing.confirm-leave.leave' | 'sharing.confirm-leave.title' | 'shortcuts-dialog.collaboration' | 'shortcuts-dialog.edit' | 'shortcuts-dialog.file' | 'shortcuts-dialog.preferences' | 'shortcuts-dialog.title' | 'shortcuts-dialog.tools' | 'shortcuts-dialog.transform' | 'shortcuts-dialog.view' | 'size-style.l' | 'size-style.m' | 'size-style.s' | 'size-style.xl' | 'spline-style.cubic' | 'spline-style.line' | 'status.offline' | 'status.online' | 'style-panel.align' | 'style-panel.arrowhead-end' | 'style-panel.arrowhead-start' | 'style-panel.arrowheads' | 'style-panel.color' | 'style-panel.dash' | 'style-panel.fill' | 'style-panel.font' | 'style-panel.geo' | 'style-panel.mixed' | 'style-panel.opacity' | 'style-panel.position' | 'style-panel.size' | 'style-panel.spline' | 'style-panel.title' | 'style-panel.vertical-align' | 'toast.close' | 'toast.error.copy-fail.desc' | 'toast.error.copy-fail.title' | 'toast.error.export-fail.desc' | 'toast.error.export-fail.title' | 'tool-panel.drawing' | 'tool-panel.more' | 'tool-panel.shapes' | 'tool.arrow-down' | 'tool.arrow-left' | 'tool.arrow-right' | 'tool.arrow-up' | 'tool.arrow' | 'tool.asset' | 'tool.check-box' | 'tool.cloud' | 'tool.diamond' | 'tool.draw' | 'tool.ellipse' | 'tool.embed' | 'tool.eraser' | 'tool.frame' | 'tool.hand' | 'tool.hexagon' | 'tool.highlight' | 'tool.laser' | 'tool.line' | 'tool.note' | 'tool.octagon' | 'tool.oval' | 'tool.pentagon' | 'tool.rectangle' | 'tool.rhombus' | 'tool.select' | 'tool.star' | 'tool.text' | 'tool.trapezoid' | 'tool.triangle' | 'tool.x-box' | 'verticalAlign-style.end' | 'verticalAlign-style.middle' | 'verticalAlign-style.start' | 'vscode.file-open.backup-failed' | 'vscode.file-open.backup-saved' | 'vscode.file-open.backup' | 'vscode.file-open.desc' | 'vscode.file-open.dont-show-again' | 'vscode.file-open.open'; // @public (undocumented) export interface TLUiTranslationProviderProps { @@ -3146,9 +3148,6 @@ export interface TLV1VideoShape extends TLV1BaseShape { // @public (undocumented) export function ToggleAutoSizeMenuItem(): JSX_2.Element | null; -// @public (undocumented) -export function ToggleDarkModeItem(): JSX_2.Element; - // @public (undocumented) export function ToggleDebugModeItem(): JSX_2.Element; diff --git a/packages/tldraw/src/index.ts b/packages/tldraw/src/index.ts index ffa6dcd85..bdfde9bb5 100644 --- a/packages/tldraw/src/index.ts +++ b/packages/tldraw/src/index.ts @@ -222,7 +222,6 @@ export { ReorderMenuSubmenu, SelectAllMenuItem, ToggleAutoSizeMenuItem, - ToggleDarkModeItem, ToggleDebugModeItem, ToggleEdgeScrollingItem, ToggleFocusModeItem, diff --git a/packages/tldraw/src/lib/ui/components/ColorSchemeMenu.tsx b/packages/tldraw/src/lib/ui/components/ColorSchemeMenu.tsx new file mode 100644 index 000000000..10bb0309a --- /dev/null +++ b/packages/tldraw/src/lib/ui/components/ColorSchemeMenu.tsx @@ -0,0 +1,41 @@ +import { useEditor, useValue } from '@tldraw/editor' +import { useUiEvents } from '../context/events' +import { TldrawUiMenuCheckboxItem } from './primitives/menus/TldrawUiMenuCheckboxItem' +import { TldrawUiMenuGroup } from './primitives/menus/TldrawUiMenuGroup' +import { TldrawUiMenuSubmenu } from './primitives/menus/TldrawUiMenuSubmenu' + +const COLOR_SCHEMES = [ + { colorScheme: 'light' as const, label: 'color-scheme.light' }, + { colorScheme: 'dark' as const, label: 'color-scheme.dark' }, + { colorScheme: 'system' as const, label: 'color-scheme.system' }, +] + +/** @public @react */ +export function ColorSchemeMenu() { + const editor = useEditor() + const trackEvent = useUiEvents() + const currentColorScheme = useValue( + 'colorScheme', + () => editor.user.getUserPreferences().colorScheme, + [editor] + ) + + return ( + + + {COLOR_SCHEMES.map(({ colorScheme, label }) => ( + { + editor.user.updateUserPreferences({ colorScheme }) + trackEvent('color-scheme', { source: 'menu', value: colorScheme }) + }} + /> + ))} + + + ) +} diff --git a/packages/tldraw/src/lib/ui/components/MainMenu/DefaultMainMenuContent.tsx b/packages/tldraw/src/lib/ui/components/MainMenu/DefaultMainMenuContent.tsx index 4bd0e508f..99198dd69 100644 --- a/packages/tldraw/src/lib/ui/components/MainMenu/DefaultMainMenuContent.tsx +++ b/packages/tldraw/src/lib/ui/components/MainMenu/DefaultMainMenuContent.tsx @@ -1,6 +1,7 @@ import { useEditor, useValue } from '@tldraw/editor' import { useActions } from '../../context/actions' import { useCanRedo, useCanUndo } from '../../hooks/menu-hooks' +import { ColorSchemeMenu } from '../ColorSchemeMenu' import { LanguageMenu } from '../LanguageMenu' import { ClipboardMenuGroup, @@ -14,7 +15,6 @@ import { RemoveFrameMenuItem, SelectAllMenuItem, ToggleAutoSizeMenuItem, - ToggleDarkModeItem, ToggleDebugModeItem, ToggleDynamicSizeModeItem, ToggleEdgeScrollingItem, @@ -170,13 +170,15 @@ export function PreferencesGroup() { - + + + diff --git a/packages/tldraw/src/lib/ui/context/actions.tsx b/packages/tldraw/src/lib/ui/context/actions.tsx index e292e3eb6..764144262 100644 --- a/packages/tldraw/src/lib/ui/context/actions.tsx +++ b/packages/tldraw/src/lib/ui/context/actions.tsx @@ -1110,8 +1110,11 @@ export function ActionsProvider({ overrides, children }: ActionsProviderProps) { kbd: '$/', readonlyOk: true, onSelect(source) { - trackEvent('toggle-dark-mode', { source }) - editor.user.updateUserPreferences({ isDarkMode: !editor.user.getIsDarkMode() }) + const value = editor.user.getIsDarkMode() ? 'light' : 'dark' + trackEvent('color-scheme', { source, value }) + editor.user.updateUserPreferences({ + colorScheme: value, + }) }, checkbox: true, }, diff --git a/packages/tldraw/src/lib/ui/context/events.tsx b/packages/tldraw/src/lib/ui/context/events.tsx index 2be5ead95..8aa8ffc09 100644 --- a/packages/tldraw/src/lib/ui/context/events.tsx +++ b/packages/tldraw/src/lib/ui/context/events.tsx @@ -84,7 +84,6 @@ export interface TLUiEventMap { 'toggle-snap-mode': null 'toggle-tool-lock': null 'toggle-grid-mode': null - 'toggle-dark-mode': null 'toggle-wrap-mode': null 'toggle-focus-mode': null 'toggle-debug-mode': null @@ -92,6 +91,7 @@ export interface TLUiEventMap { 'toggle-lock': null 'toggle-reduce-motion': null 'toggle-edge-scrolling': null + 'color-scheme': { value: string } 'exit-pen-mode': null 'stop-following': null 'open-cursor-chat': null diff --git a/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts b/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts index cbe7594d5..e7b7dc2dd 100644 --- a/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts +++ b/packages/tldraw/src/lib/ui/hooks/useTranslation/TLUiTranslationKey.ts @@ -119,6 +119,9 @@ export type TLUiTranslationKey = | 'action.zoom-to-selection' | 'assets.files.upload-failed' | 'assets.url.failed' + | 'color-scheme.dark' + | 'color-scheme.light' + | 'color-scheme.system' | 'color-style.white' | 'color-style.black' | 'color-style.blue' @@ -232,6 +235,7 @@ export type TLUiTranslationKey = | 'tool.embed' | 'tool.text' | 'menu.title' + | 'menu.color-scheme' | 'menu.copy-as' | 'menu.edit' | 'menu.export-as' diff --git a/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts b/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts index 546a930f8..3fa4571bb 100644 --- a/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts +++ b/packages/tldraw/src/lib/ui/hooks/useTranslation/defaultTranslation.ts @@ -119,6 +119,9 @@ export const DEFAULT_TRANSLATION = { 'action.zoom-to-selection': 'Zoom to selection', 'assets.files.upload-failed': 'Upload failed', 'assets.url.failed': "Couldn't load URL preview", + 'color-scheme.dark': 'Dark', + 'color-scheme.light': 'Light', + 'color-scheme.system': 'System', 'color-style.white': 'White', 'color-style.black': 'Black', 'color-style.blue': 'Blue', @@ -232,6 +235,7 @@ export const DEFAULT_TRANSLATION = { 'tool.embed': 'Embed', 'tool.text': 'Text', 'menu.title': 'Menu', + 'menu.color-scheme': 'Color scheme', 'menu.copy-as': 'Copy as', 'menu.edit': 'Edit', 'menu.export-as': 'Export as', diff --git a/packages/tldraw/src/lib/utils/tldr/file.ts b/packages/tldraw/src/lib/utils/tldr/file.ts index 479ee2548..e1ad49f91 100644 --- a/packages/tldraw/src/lib/utils/tldr/file.ts +++ b/packages/tldraw/src/lib/utils/tldr/file.ts @@ -318,5 +318,5 @@ export async function parseAndLoadDocument( editor.updateInstanceState({ isFocused }) }) - if (forceDarkMode) editor.user.updateUserPreferences({ isDarkMode: true }) + if (forceDarkMode) editor.user.updateUserPreferences({ colorScheme: 'dark' }) } diff --git a/packages/tldraw/src/test/TLUserPreferences.test.ts b/packages/tldraw/src/test/TLUserPreferences.test.ts index b38cfd93a..692eeb0ab 100644 --- a/packages/tldraw/src/test/TLUserPreferences.test.ts +++ b/packages/tldraw/src/test/TLUserPreferences.test.ts @@ -21,7 +21,7 @@ describe('TLUserPreferences', () => { animationSpeed: 1, color: '#000000', id: '123', - isDarkMode: true, + colorScheme: 'dark', isSnapMode: false, locale: 'en', name: 'test', @@ -38,15 +38,15 @@ describe('TLUserPreferences', () => { userPreferences.set({ ...userPreferences.get(), - isDarkMode: false, + colorScheme: 'light', }) expect(editor.user.getIsDarkMode()).toBe(false) - editor.user.updateUserPreferences({ isDarkMode: true }) + editor.user.updateUserPreferences({ colorScheme: 'dark' }) expect(editor.user.getIsDarkMode()).toBe(true) - expect(userPreferences.get().isDarkMode).toBe(true) + expect(userPreferences.get().colorScheme).toBe('dark') }) it('can have null values and it will use defaults', () => { @@ -54,7 +54,7 @@ describe('TLUserPreferences', () => { id: '123', animationSpeed: null, color: null, - isDarkMode: null, + colorScheme: 'system', isSnapMode: null, locale: null, name: null,