fix: sync whiteboard theme with app mode

This commit is contained in:
lucaronin
2026-05-08 13:04:24 +02:00
parent 4fd0a265f4
commit 91e02f0719
4 changed files with 117 additions and 3 deletions

View File

@@ -596,6 +596,7 @@ Defined in `src/utils/durableMarkdownBlocks.ts`, `src/utils/editorDurableMarkdow
- Each `tldrawBlock` stores a stable `boardId` plus the tldraw document snapshot JSON. Session state such as camera, selected tool, and current selection is not persisted into the note.
- The rich editor renders the block with the `tldraw` package and saves debounced document snapshot changes back into the block props, so normal Tolaria autosave writes the board into the `.md` file.
- Whiteboard prop writes re-resolve the live BlockNote block by id before mutating it, and disappear as no-ops if a note reload or mode switch has already removed that block.
- The tldraw runtime receives Tolaria's resolved light/dark mode as its user color scheme, so embedded whiteboards follow the app appearance and update while mounted.
- Mermaid and tldraw both register small codecs with the shared durable fenced-block pipeline; scanner, token, block injection, and mixed serialization mechanics live in one owner.
- The `/whiteboard` slash command inserts an empty tldraw block using the same Markdown-durable storage path. Preview images are intentionally omitted; thumbnails can be added later as derived cache artifacts.

View File

@@ -1,11 +1,18 @@
import { render, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { act, cleanup, render, screen, waitFor } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { Editor } from 'tldraw'
import { TldrawWhiteboard } from './TldrawWhiteboard'
interface MockTldrawProps {
assetUrls: MockAssetUrls
onMount: (editor: Editor) => () => void
user?: MockTldrawUser
}
interface MockTldrawUser {
userPreferences: {
get: () => { colorScheme?: string }
}
}
interface MockAssetUrls {
@@ -64,8 +71,18 @@ vi.mock('tldraw', async () => {
createTLStore: vi.fn(() => ({
listen: vi.fn(() => vi.fn()),
})),
defaultUserPreferences: {
colorScheme: 'light',
locale: 'en',
},
getSnapshot: vi.fn(() => ({ document: {} })),
loadSnapshot: vi.fn(),
useTldrawUser: vi.fn(({ userPreferences }: { userPreferences: { colorScheme: string } }) => ({
setUserPreferences: vi.fn(),
userPreferences: {
get: () => userPreferences,
},
})),
}
})
@@ -76,7 +93,7 @@ function renderedTldrawAssetUrls(): MockAssetUrls {
}
function renderedTldrawProps(): MockTldrawProps {
const props = tldrawMock.Tldraw.mock.calls[0]?.[0] as MockTldrawProps
const props = tldrawMock.Tldraw.mock.lastCall?.[0] as MockTldrawProps
expect(props).toBeDefined()
return props
}
@@ -125,6 +142,13 @@ function expectBundledTldrawAssetUrls(assetUrls: MockAssetUrls) {
}
describe('TldrawWhiteboard', () => {
afterEach(() => {
cleanup()
document.documentElement.removeAttribute('data-theme')
document.documentElement.classList.remove('dark')
vi.clearAllMocks()
})
it('uses bundled tldraw assets instead of CDN URLs', () => {
render(
<TldrawWhiteboard
@@ -142,6 +166,50 @@ describe('TldrawWhiteboard', () => {
expectBundledTldrawAssetUrls(renderedTldrawAssetUrls())
})
it('passes Tolaria dark mode to tldraw', () => {
document.documentElement.setAttribute('data-theme', 'dark')
document.documentElement.classList.add('dark')
render(
<TldrawWhiteboard
boardId="board-1"
height="520"
snapshot=""
width=""
onSizeChange={vi.fn()}
onSnapshotChange={vi.fn()}
/>
)
expect(renderedTldrawProps().user?.userPreferences.get().colorScheme).toBe('dark')
})
it('updates the tldraw color scheme when Tolaria theme changes', async () => {
document.documentElement.setAttribute('data-theme', 'light')
render(
<TldrawWhiteboard
boardId="board-1"
height="520"
snapshot=""
width=""
onSizeChange={vi.fn()}
onSnapshotChange={vi.fn()}
/>
)
expect(renderedTldrawProps().user?.userPreferences.get().colorScheme).toBe('light')
act(() => {
document.documentElement.setAttribute('data-theme', 'dark')
document.documentElement.classList.add('dark')
})
await waitFor(() => {
expect(renderedTldrawProps().user?.userPreferences.get().colorScheme).toBe('dark')
})
})
it('installs the text measurement guard when the canvas mounts', () => {
render(
<TldrawWhiteboard

View File

@@ -9,20 +9,26 @@ import {
TldrawUiMenuContextProvider,
Tldraw,
createTLStore,
defaultUserPreferences,
getSnapshot,
loadSnapshot,
react as reactToTldrawSignal,
useDialogs,
useTldrawUser,
useTranslation,
type Editor,
type TLEventInfo,
type TLUiDialog,
type TLStoreSnapshot,
type TLUserPreferences,
} from 'tldraw'
import 'tldraw/tldraw.css'
import { useDocumentThemeMode } from '../hooks/useDocumentThemeMode'
import type { ResolvedThemeMode } from '../lib/themeMode'
import { installTldrawTextMeasurementGuard } from './tldrawTextMeasurementGuard'
const EMPTY_TLDRAW_TRANSLATION_URL = 'data:application/json;base64,e30K'
const TOLARIA_TLDRAW_USER_ID = 'tolaria-whiteboard'
function resolveTldrawAssetUrl(assetUrl: string | undefined): string {
return assetUrl ?? EMPTY_TLDRAW_TRANSLATION_URL
@@ -89,6 +95,18 @@ function cssSize({ height, width }: PixelSize): CSSProperties {
} as CSSProperties
}
function tldrawUserPreferences(themeMode: ResolvedThemeMode): TLUserPreferences {
return {
...defaultUserPreferences,
id: TOLARIA_TLDRAW_USER_ID,
colorScheme: themeMode,
}
}
function ignoreTldrawUserPreferencesUpdate(preferences: TLUserPreferences) {
void preferences
}
function parseSnapshot(source: string): TLStoreSnapshot | null {
if (!source.trim()) return null
@@ -387,6 +405,12 @@ export function TldrawWhiteboard({
const persistedSize = useMemo(() => normalizeSize({ height, width }), [height, width])
const [resizingSize, setResizingSize] = useState<PixelSize | null>(null)
const visibleSize = resizingSize ?? persistedSize
const themeMode = useDocumentThemeMode()
const userPreferences = useMemo(() => tldrawUserPreferences(themeMode), [themeMode])
const tldrawUser = useTldrawUser({
setUserPreferences: ignoreTldrawUserPreferencesUpdate,
userPreferences,
})
useEffect(() => {
onSnapshotChangeRef.current = onSnapshotChange
@@ -483,6 +507,7 @@ export function TldrawWhiteboard({
components={tldrawUiComponents}
onMount={installWhiteboardRuntimeGuards}
store={store}
user={tldrawUser}
/>
<div
aria-label="Resize whiteboard width"

View File

@@ -140,6 +140,26 @@ test('tldraw whiteboard fences render as embedded canvases and remain Markdown-d
expect(rawAfterRichMode).not.toContain('@@TOLARIA_TLDRAW')
})
test('embedded tldraw whiteboards follow Tolaria theme changes', async ({ page }) => {
await openNote(page, 'Whiteboard Embed')
const tldrawContainer = page.locator('.tldraw-whiteboard .tl-container').first()
await expect(tldrawContainer).toBeVisible({ timeout: 20_000 })
const initialMode = await tldrawContainer.evaluate((element) =>
element.classList.contains('tl-theme__dark') ? 'dark' : 'light'
)
await page.getByTestId('status-theme-mode').click()
const toggledMode = initialMode === 'dark' ? 'light' : 'dark'
await expect(tldrawContainer).toHaveClass(new RegExp(`tl-theme__${toggledMode}`))
await expect(tldrawContainer).toHaveAttribute('data-color-mode', toggledMode)
await page.getByTestId('status-theme-mode').click()
await expect(tldrawContainer).toHaveClass(new RegExp(`tl-theme__${initialMode}`))
await expect(tldrawContainer).toHaveAttribute('data-color-mode', initialMode)
})
test('embedded tldraw interactions stay inside the whiteboard', async ({ page }) => {
await openNote(page, 'Whiteboard Embed')