···55The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7788+## [0.16.2] — 2026-03-31
99+1010+### Added
1111+- Mobile E2E tests: 22 tests across landing, docs, and sheets for 3 device viewports (Pixel 7, iPhone 14, iPad gen 7) (#271)
1212+- Playwright mobile device projects (mobile-chrome, mobile-safari, tablet)
1313+- Unit tests for mobile CSS media query rules (sidebar overlays, link tooltip, toolbar dropdowns)
1414+1515+### Fixed
1616+- Mobile: sidebars overlay content instead of pushing layout on tablet/phone (#271)
1717+- Mobile: link preview tooltip constrained to viewport width
1818+- Mobile: toolbar dropdowns use full-width fixed overlay on phones
1919+- Mobile: find/replace bar stacks vertically on phones
2020+- Mobile: command palette height increased on phones for easier reach
2121+822## [0.16.1] — 2026-03-31
9231024### Added
+114
e2e/mobile-docs.spec.ts
···11+import { test, expect } from '@playwright/test';
22+import { createNewDoc } from './helpers';
33+44+test.describe('Mobile — Docs Editor', () => {
55+ test.beforeEach(async ({ page }) => {
66+ await createNewDoc(page);
77+ });
88+99+ test('editor is visible and fills viewport width', async ({ page }) => {
1010+ const editor = page.locator('.tiptap');
1111+ await expect(editor).toBeVisible();
1212+1313+ const viewport = page.viewportSize()!;
1414+ const box = await editor.boundingBox();
1515+ // Editor should use most of the viewport width
1616+ expect(box!.width).toBeGreaterThan(viewport.width * 0.8);
1717+ });
1818+1919+ test('toolbar buttons meet minimum touch target size', async ({ page }) => {
2020+ const viewport = page.viewportSize()!;
2121+ if (viewport.width > 768) test.skip();
2222+2323+ const buttons = page.locator('.tb-btn:visible');
2424+ const count = await buttons.count();
2525+ expect(count).toBeGreaterThan(0);
2626+2727+ for (let i = 0; i < Math.min(count, 5); i++) {
2828+ const box = await buttons.nth(i).boundingBox();
2929+ if (box) {
3030+ expect(box.width).toBeGreaterThanOrEqual(42); // 44 target, 2px tolerance
3131+ expect(box.height).toBeGreaterThanOrEqual(42);
3232+ }
3333+ }
3434+ });
3535+3636+ test('non-essential toolbar items hidden on mobile', async ({ page }) => {
3737+ const viewport = page.viewportSize()!;
3838+ if (viewport.width > 768) test.skip();
3939+4040+ const hiddenItems = page.locator('.toolbar-mobile-hide');
4141+ const count = await hiddenItems.count();
4242+ for (let i = 0; i < count; i++) {
4343+ await expect(hiddenItems.nth(i)).not.toBeVisible();
4444+ }
4545+ });
4646+4747+ test('can type and see text in editor', async ({ page }) => {
4848+ const editor = page.locator('.tiptap');
4949+ await editor.click();
5050+ await page.keyboard.type('Mobile typing test');
5151+ await expect(editor).toContainText('Mobile typing test');
5252+ });
5353+5454+ test('topbar does not overflow horizontally', async ({ page }) => {
5555+ const viewport = page.viewportSize()!;
5656+ const topbar = page.locator('.app-topbar');
5757+ const box = await topbar.boundingBox();
5858+ if (box) {
5959+ expect(box.width).toBeLessThanOrEqual(viewport.width + 1);
6060+ }
6161+ });
6262+6363+ test('document title input is accessible', async ({ page }) => {
6464+ const titleInput = page.locator('.doc-title-input');
6565+ await expect(titleInput).toBeVisible();
6666+ await titleInput.fill('My Mobile Doc');
6767+ await expect(titleInput).toHaveValue('My Mobile Doc');
6868+ });
6969+7070+ test('word count visible in footer', async ({ page }) => {
7171+ const editor = page.locator('.tiptap');
7272+ await editor.click();
7373+ await page.keyboard.type('one two three');
7474+7575+ await expect(page.locator('#word-count')).toContainText('3 words', { timeout: 5000 });
7676+ });
7777+7878+ test('command palette opens and is usable', async ({ page }) => {
7979+ const viewport = page.viewportSize()!;
8080+8181+ await page.keyboard.press('Meta+k');
8282+ const palette = page.locator('.cmd-palette');
8383+ await expect(palette).toBeVisible({ timeout: 3000 });
8484+8585+ // Palette should fit within viewport
8686+ const box = await palette.boundingBox();
8787+ if (box) {
8888+ expect(box.right).toBeLessThanOrEqual(viewport.width + 5);
8989+ expect(box.bottom).toBeLessThanOrEqual(viewport.height + 5);
9090+ }
9191+9292+ // Dismiss
9393+ await page.keyboard.press('Escape');
9494+ await expect(palette).not.toBeVisible();
9595+ });
9696+9797+ test('modals are full-screen on phones', async ({ page }) => {
9898+ const viewport = page.viewportSize()!;
9999+ if (viewport.width > 480) test.skip();
100100+101101+ // Open share dialog as a test modal
102102+ await page.click('#btn-share');
103103+ const dialog = page.locator('#share-dialog');
104104+ await expect(dialog).toBeVisible({ timeout: 3000 });
105105+106106+ const box = await dialog.boundingBox();
107107+ if (box) {
108108+ // Should be approximately full-width on phones
109109+ expect(box.width).toBeGreaterThan(viewport.width * 0.9);
110110+ }
111111+112112+ await page.click('#share-dialog-close');
113113+ });
114114+});