feat: add whiteboard fullscreen workspace

This commit is contained in:
lucaronin
2026-06-05 20:01:55 +02:00
parent 12a229e604
commit 95bdbc90c7
24 changed files with 221 additions and 70 deletions

View File

@@ -629,6 +629,7 @@ Defined in `src/utils/durableMarkdownBlocks.ts`, `src/utils/editorDurableMarkdow
- 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.
- Embedded whiteboards expose a session-only full-window workspace that reuses the same tldraw store and Markdown snapshot; expanding or closing it does not persist camera, tool, or size state.
- 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

@@ -534,6 +534,8 @@ files:
editor.toolbar.exportPdf: 7a39acf8742a59d1a4f7ae064b6219a0
editor.toolbar.moreActions: 89e19353176b94068dec4c768c25e70b
editor.toolbar.openProperties: 948b90b60b8ce827f179e8c5b85b112e
editor.whiteboard.enterFullscreen: 606bf766125ecff4b33ca98122222c72
editor.whiteboard.exitFullscreen: afd0a0414d79d047f00319921e4d4944
editor.exportPdf.unavailable: 2fce9834984172b64b66767485d815e9
editor.exportPdf.failed: bf3fa78ff5725f5cbde8979b67ec2e2a
filePreview.copyDeepLink: b75833669968adbef634495636e3fe50

View File

@@ -423,6 +423,21 @@
user-select: none;
}
body.tldraw-whiteboard-fullscreen-open {
overflow: hidden;
}
.editor__blocknote-container .tldraw-whiteboard.tldraw-whiteboard--fullscreen {
position: fixed;
inset: 8px;
z-index: 1200;
width: auto;
max-width: none;
height: auto;
border-radius: 10px;
box-shadow: 0 18px 60px rgb(0 0 0 / 24%);
}
.editor__blocknote-container .tldraw-whiteboard .tl-container {
--color-background: var(--card);
--tl-layer-menu-click-capture: 25;
@@ -434,6 +449,20 @@
--tl-layer-following-indicator: 80;
}
.editor__blocknote-container .tldraw-whiteboard__fullscreen-button {
position: absolute;
top: 8px;
right: 8px;
z-index: 90;
background: var(--background);
box-shadow: 0 2px 8px rgb(0 0 0 / 12%);
}
.editor__blocknote-container .tldraw-whiteboard--fullscreen .tldraw-whiteboard__fullscreen-button {
top: 12px;
right: 12px;
}
.editor__blocknote-container .tldraw-whiteboard [data-radix-popper-content-wrapper] {
position: absolute !important;
}
@@ -451,6 +480,10 @@
touch-action: none;
}
.editor__blocknote-container .tldraw-whiteboard--fullscreen .tldraw-whiteboard__resize-handle {
display: none;
}
.editor__blocknote-container .tldraw-whiteboard__resize-handle--width {
top: 0;
right: 0;

View File

@@ -1,7 +1,9 @@
import { act, cleanup, render, screen, waitFor } from '@testing-library/react'
import type { ComponentProps } from 'react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { Editor } from 'tldraw'
import { TldrawWhiteboard } from './TldrawWhiteboard'
import { TooltipProvider } from './ui/tooltip'
interface MockTldrawProps {
assetUrls: MockAssetUrls
@@ -157,6 +159,27 @@ function expectBundledTldrawAssetUrls(assetUrls: MockAssetUrls) {
expectNoCdnUrls(assetUrls.translations)
}
function whiteboardProps(
overrides: Partial<ComponentProps<typeof TldrawWhiteboard>> = {},
): ComponentProps<typeof TldrawWhiteboard> {
return {
boardId: 'board-1',
height: '520',
snapshot: '',
width: '',
onSizeChange: vi.fn(),
onSnapshotChange: vi.fn(),
...overrides,
}
}
function renderWhiteboard(overrides: Partial<ComponentProps<typeof TldrawWhiteboard>> = {}) {
return render(
<TldrawWhiteboard {...whiteboardProps(overrides)} />,
{ wrapper: TooltipProvider },
)
}
describe('TldrawWhiteboard', () => {
afterEach(() => {
cleanup()
@@ -166,16 +189,7 @@ describe('TldrawWhiteboard', () => {
})
it('uses bundled tldraw assets instead of CDN URLs', () => {
render(
<TldrawWhiteboard
boardId="board-1"
height="520"
snapshot=""
width=""
onSizeChange={vi.fn()}
onSnapshotChange={vi.fn()}
/>
)
renderWhiteboard()
expect(screen.getByTestId('mock-tldraw')).toHaveAttribute('data-draw-font-url')
expect(assetImportMock.getAssetUrlsByImport).toHaveBeenCalledWith(expect.any(Function))
@@ -186,16 +200,7 @@ describe('TldrawWhiteboard', () => {
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()}
/>
)
renderWhiteboard()
expect(renderedTldrawProps().user?.userPreferences.get().colorScheme).toBe('dark')
})
@@ -203,16 +208,7 @@ describe('TldrawWhiteboard', () => {
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()}
/>
)
renderWhiteboard()
expect(renderedTldrawProps().user?.userPreferences.get().colorScheme).toBe('light')
@@ -227,16 +223,7 @@ describe('TldrawWhiteboard', () => {
})
it('installs the text measurement guard when the canvas mounts', () => {
render(
<TldrawWhiteboard
boardId="board-1"
height="520"
snapshot=""
width=""
onSizeChange={vi.fn()}
onSnapshotChange={vi.fn()}
/>
)
renderWhiteboard()
const editor = mockEditor()
const cleanup = renderedTldrawProps().onMount(editor)
@@ -254,16 +241,7 @@ describe('TldrawWhiteboard', () => {
})
it('suppresses whiteboard platform permission rejections while mounted', () => {
render(
<TldrawWhiteboard
boardId="board-1"
height="520"
snapshot=""
width=""
onSizeChange={vi.fn()}
onSnapshotChange={vi.fn()}
/>
)
renderWhiteboard()
const cleanup = renderedTldrawProps().onMount(mockEditor())
const denied = {
@@ -280,28 +258,12 @@ describe('TldrawWhiteboard', () => {
it('resets the drawing store when switching to a blank board snapshot', () => {
const boardASnapshot = { records: { shape: 'from-board-a' } }
const { rerender } = render(
<TldrawWhiteboard
boardId="board-1"
height="520"
snapshot={JSON.stringify(boardASnapshot)}
width=""
onSizeChange={vi.fn()}
onSnapshotChange={vi.fn()}
/>
)
const { rerender } = renderWhiteboard({ snapshot: JSON.stringify(boardASnapshot) })
expect(tldrawStoreMock.loadSnapshot).toHaveBeenLastCalledWith(expect.any(Object), boardASnapshot)
rerender(
<TldrawWhiteboard
boardId="board-2"
height="520"
snapshot=""
width=""
onSizeChange={vi.fn()}
onSnapshotChange={vi.fn()}
/>
<TldrawWhiteboard {...whiteboardProps({ boardId: 'board-2' })} />
)
expect(tldrawStoreMock.loadSnapshot).toHaveBeenLastCalledWith(expect.any(Object), { records: {} })

View File

@@ -1,5 +1,6 @@
import { memo, useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type MutableRefObject, type PointerEvent as ReactPointerEvent } from 'react'
import { memo, useCallback, useEffect, useMemo, useRef, useState, type CSSProperties, type MouseEvent as ReactMouseEvent, type MutableRefObject, type PointerEvent as ReactPointerEvent } from 'react'
import { getAssetUrlsByImport } from '@tldraw/assets/imports.vite'
import { ArrowsIn, ArrowsOut } from '@phosphor-icons/react'
import { Dialog as DialogPrimitive } from 'radix-ui'
import {
Box,
@@ -19,11 +20,15 @@ import {
} from 'tldraw'
import 'tldraw/tldraw.css'
import { useDocumentThemeMode } from '../hooks/useDocumentThemeMode'
import { resolveEffectiveLocale, translate, type AppLocale } from '../lib/i18n'
import type { ResolvedThemeMode } from '../lib/themeMode'
import { Button } from './ui/button'
import { ActionTooltip } from './ui/action-tooltip'
import { installTldrawTextMeasurementGuard } from './tldrawTextMeasurementGuard'
const EMPTY_TLDRAW_TRANSLATION_URL = 'data:application/json;base64,e30K'
const TOLARIA_TLDRAW_USER_ID = 'tolaria-whiteboard'
const WHITEBOARD_FULLSCREEN_BODY_CLASS = 'tldraw-whiteboard-fullscreen-open'
function resolveTldrawAssetUrl(assetUrl: string | undefined): string {
return assetUrl ?? EMPTY_TLDRAW_TRANSLATION_URL
@@ -101,6 +106,28 @@ function ignoreTldrawUserPreferencesUpdate(preferences: TLUserPreferences) {
void preferences
}
function readDocumentLocale(): AppLocale {
if (typeof document === 'undefined') return 'en'
return resolveEffectiveLocale(document.documentElement.lang)
}
function useDocumentLocale(): AppLocale {
const [locale, setLocale] = useState(readDocumentLocale)
useEffect(() => {
if (typeof document === 'undefined') return
const syncLocale = () => setLocale(readDocumentLocale())
const observer = new MutationObserver(syncLocale)
observer.observe(document.documentElement, { attributeFilter: ['lang'], attributes: true })
syncLocale()
return () => observer.disconnect()
}, [])
return locale
}
function rejectionName(error: unknown): string {
if (error instanceof Error) return error.name
if (typeof error !== 'object' || error === null || !('name' in error)) return ''
@@ -425,6 +452,40 @@ function TolariaTldrawDialogs() {
))
}
function useFullscreenWhiteboard() {
const [fullscreen, setFullscreen] = useState(false)
useEffect(() => {
if (!fullscreen) return
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === 'Escape') setFullscreen(false)
}
document.body.classList.add(WHITEBOARD_FULLSCREEN_BODY_CLASS)
window.addEventListener('keydown', handleKeyDown)
return () => {
document.body.classList.remove(WHITEBOARD_FULLSCREEN_BODY_CLASS)
window.removeEventListener('keydown', handleKeyDown)
}
}, [fullscreen])
useEffect(() => {
const animationFrameId = window.requestAnimationFrame(() => {
window.dispatchEvent(new Event('resize'))
})
return () => { window.cancelAnimationFrame(animationFrameId) }
}, [fullscreen])
const toggleFullscreen = useCallback((event: ReactMouseEvent<HTMLButtonElement>) => {
event.preventDefault()
event.stopPropagation()
setFullscreen((current) => !current)
}, [])
return { fullscreen, toggleFullscreen }
}
export function TldrawWhiteboard({
boardId,
height,
@@ -441,6 +502,12 @@ export function TldrawWhiteboard({
const persistedSize = useMemo(() => normalizeSize({ height, width }), [height, width])
const [resizingSize, setResizingSize] = useState<PixelSize | null>(null)
const visibleSize = resizingSize ?? persistedSize
const { fullscreen, toggleFullscreen } = useFullscreenWhiteboard()
const locale = useDocumentLocale()
const fullscreenLabel = translate(
locale,
fullscreen ? 'editor.whiteboard.exitFullscreen' : 'editor.whiteboard.enterFullscreen',
)
const themeMode = useDocumentThemeMode()
const userPreferences = useMemo(() => tldrawUserPreferences(themeMode), [themeMode])
const tldrawUser = useTldrawUser({
@@ -540,7 +607,7 @@ export function TldrawWhiteboard({
return (
<div
ref={boardRef}
className="tldraw-whiteboard"
className={fullscreen ? 'tldraw-whiteboard tldraw-whiteboard--fullscreen' : 'tldraw-whiteboard'}
contentEditable={false}
data-board-id={boardId}
style={cssSize(visibleSize)}
@@ -553,6 +620,21 @@ export function TldrawWhiteboard({
store={store}
user={tldrawUser}
/>
<ActionTooltip copy={{ label: fullscreenLabel }} side="left">
<Button
type="button"
variant="outline"
size="icon-xs"
aria-label={fullscreenLabel}
aria-pressed={fullscreen}
className="tldraw-whiteboard__fullscreen-button"
data-testid="tldraw-whiteboard-fullscreen-toggle"
title={fullscreenLabel}
onClick={toggleFullscreen}
>
{fullscreen ? <ArrowsIn aria-hidden="true" /> : <ArrowsOut aria-hidden="true" />}
</Button>
</ActionTooltip>
<button
type="button"
aria-label="Resize whiteboard width"

View File

@@ -532,6 +532,8 @@
"editor.toolbar.exportPdf": "Экспартаваць нататку ў фармаце PDF",
"editor.toolbar.moreActions": "Больш дзеянняў",
"editor.toolbar.openProperties": "Адкрыць уласцівасці",
"editor.whiteboard.enterFullscreen": "Разгарнуць дошку",
"editor.whiteboard.exitFullscreen": "Выйсці з поўнаэкраннай дошкі",
"editor.exportPdf.unavailable": "Адкрыйце нататку Markdown перад экспартам у PDF.",
"editor.exportPdf.failed": "Немагчыма пачаць экспарт PDF: {error}",
"filePreview.copyDeepLink": "Капіраваць спасылку",

View File

@@ -534,6 +534,8 @@
"editor.toolbar.exportPdf": "Ekspartavać natatku ŭ farmacie PDF",
"editor.toolbar.moreActions": "Boĺš dziejanniaŭ",
"editor.toolbar.openProperties": "Adkryć ulascivasci",
"editor.whiteboard.enterFullscreen": "Pašyryć došku",
"editor.whiteboard.exitFullscreen": "Vyjsci z poŭnaekrannaj doški",
"editor.exportPdf.unavailable": "Adkryjcie natatku Markdown pierad ekspartam u PDF.",
"editor.exportPdf.failed": "Nie ŭdalosia pačać ekspart PDF: {error}",
"filePreview.copyDeepLink": "Kapijavać spasylku",

View File

@@ -532,6 +532,8 @@
"editor.toolbar.exportPdf": "Notiz als PDF exportieren",
"editor.toolbar.moreActions": "Weitere Notizaktionen",
"editor.toolbar.openProperties": "Eigenschaften-Panel öffnen",
"editor.whiteboard.enterFullscreen": "Whiteboard erweitern",
"editor.whiteboard.exitFullscreen": "Vollbild-Whiteboard verlassen",
"editor.exportPdf.unavailable": "Öffne eine Markdown-Notiz, bevor du sie als PDF exportierst.",
"editor.exportPdf.failed": "PDF-Export konnte nicht gestartet werden: {error}",
"filePreview.copyDeepLink": "Link kopieren",

View File

@@ -532,6 +532,8 @@
"editor.toolbar.exportPdf": "Export note as PDF",
"editor.toolbar.moreActions": "More note actions",
"editor.toolbar.openProperties": "Open the properties panel",
"editor.whiteboard.enterFullscreen": "Expand whiteboard",
"editor.whiteboard.exitFullscreen": "Exit fullscreen whiteboard",
"editor.exportPdf.unavailable": "Open a Markdown note before exporting a PDF.",
"editor.exportPdf.failed": "Could not start PDF export: {error}",
"filePreview.copyDeepLink": "Copy link",

View File

@@ -532,6 +532,8 @@
"editor.toolbar.exportPdf": "Exportar nota como PDF",
"editor.toolbar.moreActions": "Más acciones de nota",
"editor.toolbar.openProperties": "Abrir el panel de propiedades",
"editor.whiteboard.enterFullscreen": "Expandir pizarra",
"editor.whiteboard.exitFullscreen": "Salir de la pizarra en pantalla completa",
"editor.exportPdf.unavailable": "Abre una nota de Markdown antes de exportar un PDF.",
"editor.exportPdf.failed": "No se pudo iniciar la exportación a PDF: {error}",
"filePreview.copyDeepLink": "Copiar enlace",

View File

@@ -532,6 +532,8 @@
"editor.toolbar.exportPdf": "Exportar nota como PDF",
"editor.toolbar.moreActions": "Más acciones de nota",
"editor.toolbar.openProperties": "Abrir el panel de propiedades",
"editor.whiteboard.enterFullscreen": "Expandir pizarra",
"editor.whiteboard.exitFullscreen": "Salir de la pizarra en pantalla completa",
"editor.exportPdf.unavailable": "Abre una nota de Markdown antes de exportar un PDF.",
"editor.exportPdf.failed": "No se ha podido iniciar la exportación a PDF: {error}",
"filePreview.copyDeepLink": "Copiar enlace",

View File

@@ -532,6 +532,8 @@
"editor.toolbar.exportPdf": "Exporter la note au format PDF",
"editor.toolbar.moreActions": "Autres actions de note",
"editor.toolbar.openProperties": "Ouvrir le panneau des propriétés",
"editor.whiteboard.enterFullscreen": "Agrandir le tableau blanc",
"editor.whiteboard.exitFullscreen": "Quitter le tableau blanc en plein écran",
"editor.exportPdf.unavailable": "Ouvrez une note Markdown avant d'exporter un PDF.",
"editor.exportPdf.failed": "Impossible de lancer l'exportation PDF : {error}",
"filePreview.copyDeepLink": "Copier le lien",

View File

@@ -532,6 +532,8 @@
"editor.toolbar.exportPdf": "Ekspor catatan sebagai PDF",
"editor.toolbar.moreActions": "Tindakan catatan lainnya",
"editor.toolbar.openProperties": "Buka panel properti",
"editor.whiteboard.enterFullscreen": "Perluas papan tulis",
"editor.whiteboard.exitFullscreen": "Keluar dari papan tulis layar penuh",
"editor.exportPdf.unavailable": "Buka catatan Markdown sebelum mengekspor PDF.",
"editor.exportPdf.failed": "Tidak dapat memulai ekspor PDF: {error}",
"filePreview.copyDeepLink": "Salin tautan",

View File

@@ -532,6 +532,8 @@
"editor.toolbar.exportPdf": "Esporta nota in PDF",
"editor.toolbar.moreActions": "Altre azioni della nota",
"editor.toolbar.openProperties": "Apri il pannello delle proprietà",
"editor.whiteboard.enterFullscreen": "Espandi la lavagna",
"editor.whiteboard.exitFullscreen": "Esci dalla lavagna a schermo intero",
"editor.exportPdf.unavailable": "Apri una nota Markdown prima di esportare un PDF.",
"editor.exportPdf.failed": "Impossibile avviare l'esportazione in PDF: {error}",
"filePreview.copyDeepLink": "Copia link",

View File

@@ -532,6 +532,8 @@
"editor.toolbar.exportPdf": "ートをPDFとしてエクスポート",
"editor.toolbar.moreActions": "その他のノート操作",
"editor.toolbar.openProperties": "プロパティパネルを開く",
"editor.whiteboard.enterFullscreen": "ホワイトボードを展開",
"editor.whiteboard.exitFullscreen": "フルスクリーンホワイトボードを終了",
"editor.exportPdf.unavailable": "PDFをエクスポートする前に、Markdownートを開いてください。",
"editor.exportPdf.failed": "PDFエクスポートを開始できませんでした{error}",
"filePreview.copyDeepLink": "リンクをコピー",

View File

@@ -532,6 +532,8 @@
"editor.toolbar.exportPdf": "노트를 PDF로 내보내기",
"editor.toolbar.moreActions": "추가 노트 작업",
"editor.toolbar.openProperties": "속성 패널 열기",
"editor.whiteboard.enterFullscreen": "화이트보드 펼치기",
"editor.whiteboard.exitFullscreen": "전체 화면 화이트보드에서 나가기",
"editor.exportPdf.unavailable": "PDF로 내보내기 전에 Markdown 노트를 여세요.",
"editor.exportPdf.failed": "PDF 내보내기를 시작할 수 없습니다: {error}",
"filePreview.copyDeepLink": "링크 복사",

View File

@@ -532,6 +532,8 @@
"editor.toolbar.exportPdf": "Eksportuj notatkę jako PDF",
"editor.toolbar.moreActions": "Więcej działań notatki",
"editor.toolbar.openProperties": "Otwórz panel właściwości",
"editor.whiteboard.enterFullscreen": "Rozwiń tablicę",
"editor.whiteboard.exitFullscreen": "Wyjdź z tablicy pełnoekranowej",
"editor.exportPdf.unavailable": "Otwórz notatkę Markdown przed wyeksportowaniem pliku PDF.",
"editor.exportPdf.failed": "Nie można rozpocząć eksportu do PDF: {error}",
"filePreview.copyDeepLink": "Kopiuj link",

View File

@@ -532,6 +532,8 @@
"editor.toolbar.exportPdf": "Exportar nota como PDF",
"editor.toolbar.moreActions": "Mais ações da nota",
"editor.toolbar.openProperties": "Abrir o painel de propriedades",
"editor.whiteboard.enterFullscreen": "Expandir quadro branco",
"editor.whiteboard.exitFullscreen": "Sair do quadro branco em tela cheia",
"editor.exportPdf.unavailable": "Abra uma nota Markdown antes de exportar um PDF.",
"editor.exportPdf.failed": "Não foi possível iniciar a exportação para PDF: {error}",
"filePreview.copyDeepLink": "Copiar link",

View File

@@ -532,6 +532,8 @@
"editor.toolbar.exportPdf": "Exportar nota como PDF",
"editor.toolbar.moreActions": "Mais ações da nota",
"editor.toolbar.openProperties": "Abrir o painel de propriedades",
"editor.whiteboard.enterFullscreen": "Expandir quadro branco",
"editor.whiteboard.exitFullscreen": "Sair do quadro branco em ecrã inteiro",
"editor.exportPdf.unavailable": "Abra uma nota Markdown antes de exportar um PDF.",
"editor.exportPdf.failed": "Não foi possível iniciar a exportação para PDF: {error}",
"filePreview.copyDeepLink": "Copiar link",

View File

@@ -532,6 +532,8 @@
"editor.toolbar.exportPdf": "Экспортировать заметку в формате PDF",
"editor.toolbar.moreActions": "Другие действия с заметкой",
"editor.toolbar.openProperties": "Открыть панель свойств",
"editor.whiteboard.enterFullscreen": "Развернуть доску",
"editor.whiteboard.exitFullscreen": "Выйти из полноэкранной доски",
"editor.exportPdf.unavailable": "Откройте заметку Markdown перед экспортом в PDF.",
"editor.exportPdf.failed": "Не удалось начать экспорт в PDF: {error}",
"filePreview.copyDeepLink": "Копировать ссылку",

View File

@@ -532,6 +532,8 @@
"editor.toolbar.exportPdf": "Xuất ghi chú dưới dạng PDF",
"editor.toolbar.moreActions": "Thêm hành động ghi chú",
"editor.toolbar.openProperties": "Mở bảng thuộc tính",
"editor.whiteboard.enterFullscreen": "Mở rộng bảng trắng",
"editor.whiteboard.exitFullscreen": "Thoát chế độ bảng trắng toàn màn hình",
"editor.exportPdf.unavailable": "Mở ghi chú Markdown trước khi xuất PDF.",
"editor.exportPdf.failed": "Không thể bắt đầu xuất PDF: {error}",
"filePreview.copyDeepLink": "Sao chép liên kết",

View File

@@ -532,6 +532,8 @@
"editor.toolbar.exportPdf": "将笔记导出为 PDF",
"editor.toolbar.moreActions": "更多笔记操作",
"editor.toolbar.openProperties": "打开属性面板",
"editor.whiteboard.enterFullscreen": "展开白板",
"editor.whiteboard.exitFullscreen": "退出全屏白板",
"editor.exportPdf.unavailable": "在导出 PDF 之前,请先打开 Markdown 笔记。",
"editor.exportPdf.failed": "无法开始 PDF 导出:{error}",
"filePreview.copyDeepLink": "复制链接",

View File

@@ -532,6 +532,8 @@
"editor.toolbar.exportPdf": "將筆記匯出為 PDF",
"editor.toolbar.moreActions": "更多筆記操作",
"editor.toolbar.openProperties": "開啟屬性面板",
"editor.whiteboard.enterFullscreen": "展開白板",
"editor.whiteboard.exitFullscreen": "退出全螢幕白板",
"editor.exportPdf.unavailable": "在匯出 PDF 之前,請先開啟 Markdown 筆記。",
"editor.exportPdf.failed": "無法開始 PDF 匯出:{error}",
"filePreview.copyDeepLink": "複製連結",

View File

@@ -198,6 +198,41 @@ test('embedded tldraw interactions stay inside the whiteboard', async ({ page })
await expectNoEditorNodeSelection(page)
})
test('embedded tldraw whiteboards can expand to a full-window workspace', async ({ page }) => {
await openNote(page, 'Whiteboard Embed')
const whiteboard = page.locator('.tldraw-whiteboard')
await expect(whiteboard).toBeVisible({ timeout: 20_000 })
const embeddedBox = await whiteboard.boundingBox()
expect(embeddedBox).not.toBeNull()
const fullscreenToggle = page.getByTestId('tldraw-whiteboard-fullscreen-toggle')
await expect(fullscreenToggle).toBeVisible({ timeout: 5_000 })
await fullscreenToggle.click()
await expect(whiteboard).toHaveClass(/tldraw-whiteboard--fullscreen/u)
const fullscreenBox = await whiteboard.boundingBox()
const viewport = page.viewportSize()
expect(fullscreenBox).not.toBeNull()
expect(viewport).not.toBeNull()
expect(fullscreenBox!.x).toBeLessThanOrEqual(8)
expect(fullscreenBox!.y).toBeLessThanOrEqual(8)
expect(fullscreenBox!.width).toBeGreaterThanOrEqual(viewport!.width - 16)
expect(fullscreenBox!.height).toBeGreaterThanOrEqual(viewport!.height - 16)
await page.getByTestId('tools.more-button').click()
await expect(page.getByTestId('tools.more.ellipse')).toBeVisible({ timeout: 5_000 })
await expect(whiteboard).toHaveClass(/tldraw-whiteboard--fullscreen/u)
await expectNoEditorNodeSelection(page)
await fullscreenToggle.click()
await expect(whiteboard).not.toHaveClass(/tldraw-whiteboard--fullscreen/u)
const restoredBox = await whiteboard.boundingBox()
expect(restoredBox).not.toBeNull()
expect(Math.abs(restoredBox!.height - embeddedBox!.height)).toBeLessThan(4)
})
test('embedded tldraw dialogs appear and release focus when closed', async ({ page }) => {
await openNote(page, 'Whiteboard Embed')