···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.25.0] — 2026-04-08
99+1010+### Added
1111+- Wrap document encryption keys with user-derived passphrase (PBKDF2 + AES-256-GCM) before localStorage/server storage (#405)
1212+- Passphrase modal UI for key setup and unlock, with auto-migration of legacy plaintext keys
1313+- E2E tests for AI chat panel toggle and keyboard shortcut (#303)
1414+- E2E tests for wiki-link (cross-doc link) creation via `[[text]]` syntax (#303)
1515+- E2E tests for daily notes creation and reopen (#303)
1616+1717+### Fixed
1818+- CSS: add hex fallbacks for all 320 oklch() color declarations for older browser support (#408)
1919+820## [0.24.0] — 2026-04-07
9211022### Added
+105
e2e/ai-chat.spec.ts
···11+import { test, expect } from '@playwright/test';
22+import { createNewDoc, mod } from './helpers';
33+44+test.describe('AI Chat panel', () => {
55+ test('toggle AI chat sidebar via toolbar button', async ({ page }) => {
66+ await createNewDoc(page);
77+88+ // Sidebar should not be visible initially
99+ const sidebar = page.locator('#ai-chat-sidebar');
1010+ await expect(sidebar).toBeHidden();
1111+1212+ // Click the AI chat button
1313+ await page.click('#btn-ai-chat');
1414+ await expect(sidebar).toBeVisible();
1515+1616+ // Click again to close
1717+ await page.click('#btn-ai-chat');
1818+ await expect(sidebar).toBeHidden();
1919+ });
2020+2121+ test('toggle AI chat sidebar via keyboard shortcut', async ({ page }) => {
2222+ await createNewDoc(page);
2323+2424+ const sidebar = page.locator('#ai-chat-sidebar');
2525+ await expect(sidebar).toBeHidden();
2626+2727+ // Cmd+Shift+L to open
2828+ await page.keyboard.press(`${mod(page)}+Shift+l`);
2929+ await expect(sidebar).toBeVisible();
3030+3131+ // Cmd+Shift+L to close
3232+ await page.keyboard.press(`${mod(page)}+Shift+l`);
3333+ await expect(sidebar).toBeHidden();
3434+ });
3535+3636+ test('close sidebar via close button', async ({ page }) => {
3737+ await createNewDoc(page);
3838+3939+ await page.click('#btn-ai-chat');
4040+ const sidebar = page.locator('#ai-chat-sidebar');
4141+ await expect(sidebar).toBeVisible();
4242+4343+ await page.click('#ai-chat-close');
4444+ await expect(sidebar).toBeHidden();
4545+ });
4646+4747+ test('sidebar has input and send button', async ({ page }) => {
4848+ await createNewDoc(page);
4949+5050+ await page.click('#btn-ai-chat');
5151+ await expect(page.locator('#ai-chat-input')).toBeVisible();
5252+ await expect(page.locator('#ai-chat-send')).toBeVisible();
5353+ });
5454+5555+ test('settings panel toggles', async ({ page }) => {
5656+ await createNewDoc(page);
5757+5858+ await page.click('#btn-ai-chat');
5959+ const settings = page.locator('#ai-chat-settings');
6060+6161+ // Settings should be hidden initially
6262+ await expect(settings).toBeHidden();
6363+6464+ // Click settings button to show
6565+ await page.click('#ai-chat-settings-btn');
6666+ await expect(settings).toBeVisible();
6767+6868+ // Click again to hide
6969+ await page.click('#ai-chat-settings-btn');
7070+ await expect(settings).toBeHidden();
7171+ });
7272+7373+ test('settings panel has model selector and options', async ({ page }) => {
7474+ await createNewDoc(page);
7575+7676+ await page.click('#btn-ai-chat');
7777+ await page.click('#ai-chat-settings-btn');
7878+7979+ await expect(page.locator('#ai-model')).toBeVisible();
8080+ await expect(page.locator('#ai-context-toggle')).toBeVisible();
8181+ await expect(page.locator('#ai-actions-toggle')).toBeVisible();
8282+ });
8383+8484+ test('clear button resets messages', async ({ page }) => {
8585+ await createNewDoc(page);
8686+8787+ await page.click('#btn-ai-chat');
8888+ const messages = page.locator('#ai-chat-messages');
8989+9090+ // Initially the messages area should be empty or have a placeholder
9191+ await page.click('#ai-chat-clear-btn');
9292+9393+ // After clear, messages area should have no chat bubbles
9494+ await expect(messages.locator('.ai-chat-bubble')).toHaveCount(0);
9595+ });
9696+9797+ test('can type in chat input', async ({ page }) => {
9898+ await createNewDoc(page);
9999+100100+ await page.click('#btn-ai-chat');
101101+ const input = page.locator('#ai-chat-input');
102102+ await input.fill('Hello, this is a test message');
103103+ await expect(input).toHaveValue('Hello, this is a test message');
104104+ });
105105+});
+86
e2e/daily-notes.spec.ts
···11+import { test, expect } from '@playwright/test';
22+import { goToLanding } from './helpers';
33+44+test.describe('Daily notes', () => {
55+ test('daily note button exists on landing page', async ({ page }) => {
66+ await goToLanding(page);
77+88+ const btn = page.locator('#daily-note');
99+ await expect(btn).toBeVisible();
1010+ await expect(btn.locator('.create-card-title')).toHaveText("Today's Note");
1111+ });
1212+1313+ test('clicking daily note creates a new document', async ({ page }) => {
1414+ await goToLanding(page);
1515+1616+ await page.click('#daily-note');
1717+1818+ // Should navigate to a docs URL with a key fragment
1919+ await page.waitForURL(/\/docs\/[a-f0-9-]+#/, { timeout: 30000 });
2020+ });
2121+2222+ test('daily note document has today\'s date in title', async ({ page }) => {
2323+ await goToLanding(page);
2424+2525+ await page.click('#daily-note');
2626+ await page.waitForURL(/\/docs\//, { timeout: 30000 });
2727+ await page.waitForSelector('.tiptap', { timeout: 15000 });
2828+2929+ // The title input should contain today's date in YYYY-MM-DD format
3030+ const today = new Date();
3131+ const yyyy = today.getFullYear();
3232+ const mm = String(today.getMonth() + 1).padStart(2, '0');
3333+ const dd = String(today.getDate()).padStart(2, '0');
3434+ const datePrefix = `${yyyy}-${mm}-${dd}`;
3535+3636+ const titleInput = page.locator('#doc-title');
3737+ await expect(titleInput).toHaveValue(new RegExp(datePrefix), { timeout: 10000 });
3838+ });
3939+4040+ test('daily note has task list and notes sections', async ({ page }) => {
4141+ await goToLanding(page);
4242+4343+ await page.click('#daily-note');
4444+ await page.waitForURL(/\/docs\//, { timeout: 30000 });
4545+ await page.waitForSelector('.tiptap', { timeout: 15000 });
4646+4747+ const editor = page.locator('.tiptap');
4848+4949+ // Template includes "Tasks" and "Notes" headings
5050+ await expect(editor.locator('h2').filter({ hasText: 'Tasks' })).toBeVisible({ timeout: 10000 });
5151+ await expect(editor.locator('h2').filter({ hasText: 'Notes' })).toBeVisible();
5252+ });
5353+5454+ test('daily note has a task list item', async ({ page }) => {
5555+ await goToLanding(page);
5656+5757+ await page.click('#daily-note');
5858+ await page.waitForURL(/\/docs\//, { timeout: 30000 });
5959+ await page.waitForSelector('.tiptap', { timeout: 15000 });
6060+6161+ // Template includes a task list
6262+ const taskList = page.locator('.tiptap [data-type="taskList"]');
6363+ await expect(taskList).toBeVisible({ timeout: 10000 });
6464+ });
6565+6666+ test('clicking daily note again navigates to same note', async ({ page }) => {
6767+ await goToLanding(page);
6868+6969+ // Create the daily note
7070+ await page.click('#daily-note');
7171+ await page.waitForURL(/\/docs\//, { timeout: 30000 });
7272+ const firstUrl = page.url();
7373+7474+ // Go back to landing and click again
7575+ await goToLanding(page);
7676+ await page.click('#daily-note');
7777+ await page.waitForURL(/\/docs\//, { timeout: 30000 });
7878+ const secondUrl = page.url();
7979+8080+ // Should navigate to the same document (same doc ID)
8181+ const firstDocId = firstUrl.match(/\/docs\/([a-f0-9-]+)/)?.[1];
8282+ const secondDocId = secondUrl.match(/\/docs\/([a-f0-9-]+)/)?.[1];
8383+ expect(firstDocId).toBeTruthy();
8484+ expect(firstDocId).toBe(secondDocId);
8585+ });
8686+});
+91
e2e/wiki-links.spec.ts
···11+import { test, expect } from '@playwright/test';
22+import { createNewDoc } from './helpers';
33+44+test.describe('Wiki links (cross-doc links)', () => {
55+ test('typing [[text]] creates a wiki-link node', async ({ page }) => {
66+ await createNewDoc(page);
77+88+ const editor = page.locator('.tiptap');
99+ await editor.click();
1010+1111+ // Type the wiki-link syntax — TipTap InputRule triggers on closing ]]
1212+ await editor.pressSequentially('Check out [[My Document]]', { delay: 30 });
1313+1414+ // The InputRule should convert [[My Document]] into a wiki-link node
1515+ const wikiLink = editor.locator('.wiki-link');
1616+ await expect(wikiLink).toBeVisible({ timeout: 5000 });
1717+ await expect(wikiLink).toContainText('My Document');
1818+ });
1919+2020+ test('wiki-link node has correct data attributes', async ({ page }) => {
2121+ await createNewDoc(page);
2222+2323+ const editor = page.locator('.tiptap');
2424+ await editor.click();
2525+ await editor.pressSequentially('Link to [[Test Page]]', { delay: 30 });
2626+2727+ const wikiLink = editor.locator('.wiki-link');
2828+ await expect(wikiLink).toBeVisible({ timeout: 5000 });
2929+ await expect(wikiLink).toHaveAttribute('data-wiki-name', 'Test Page');
3030+ });
3131+3232+ test('wiki-link renders as an anchor element', async ({ page }) => {
3333+ await createNewDoc(page);
3434+3535+ const editor = page.locator('.tiptap');
3636+ await editor.click();
3737+ await editor.pressSequentially('See [[Another Doc]]', { delay: 30 });
3838+3939+ // Wiki links render as <a> tags
4040+ const link = editor.locator('a.wiki-link');
4141+ await expect(link).toBeVisible({ timeout: 5000 });
4242+ });
4343+4444+ test('multiple wiki-links in same paragraph', async ({ page }) => {
4545+ await createNewDoc(page);
4646+4747+ const editor = page.locator('.tiptap');
4848+ await editor.click();
4949+5050+ // Type first wiki link
5151+ await editor.pressSequentially('Link [[First Doc]] and [[Second Doc]]', { delay: 30 });
5252+5353+ const wikiLinks = editor.locator('.wiki-link');
5454+ await expect(wikiLinks).toHaveCount(2);
5555+ await expect(wikiLinks.nth(0)).toContainText('First Doc');
5656+ await expect(wikiLinks.nth(1)).toContainText('Second Doc');
5757+ });
5858+5959+ test('incomplete wiki-link syntax does not create a node', async ({ page }) => {
6060+ await createNewDoc(page);
6161+6262+ const editor = page.locator('.tiptap');
6363+ await editor.click();
6464+6565+ // Type only opening brackets — should NOT create a wiki-link
6666+ await editor.pressSequentially('Not a link [[incomplete', { delay: 30 });
6767+6868+ const wikiLinks = editor.locator('.wiki-link');
6969+ await expect(wikiLinks).toHaveCount(0);
7070+ });
7171+7272+ test('wiki-link with insertWikiLink command', async ({ page }) => {
7373+ await createNewDoc(page);
7474+7575+ // Use the editor command to insert a wiki link programmatically
7676+ await page.evaluate(() => {
7777+ const editorEl = document.querySelector('.tiptap') as any;
7878+ const editor = editorEl?.__tiptapEditor || (window as any).__tiptapEditor;
7979+ if (editor) {
8080+ editor.commands.insertWikiLink('Programmatic Link');
8181+ }
8282+ });
8383+8484+ const wikiLink = page.locator('.tiptap .wiki-link');
8585+ // This test is best-effort — if the editor ref isn't exposed, skip gracefully
8686+ const count = await wikiLink.count();
8787+ if (count > 0) {
8888+ await expect(wikiLink).toContainText('Programmatic Link');
8989+ }
9090+ });
9191+});