From 6c432a1f8789b909da5a150a9cbaf39e20e7b3b2 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sat, 13 Jun 2026 18:01:46 +0100 Subject: [PATCH 01/12] feat(paper): add AppearanceSettingsView with off/paper/paper-night/auto theme toggle --- .../src/views/AppearanceSettingsView.vue | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 frontend/taskdeck-web/src/views/AppearanceSettingsView.vue diff --git a/frontend/taskdeck-web/src/views/AppearanceSettingsView.vue b/frontend/taskdeck-web/src/views/AppearanceSettingsView.vue new file mode 100644 index 000000000..13706fb54 --- /dev/null +++ b/frontend/taskdeck-web/src/views/AppearanceSettingsView.vue @@ -0,0 +1,136 @@ + + + + + From 3358798b456473090ad27eb6349ba91e51ec6304 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sat, 13 Jun 2026 18:01:46 +0100 Subject: [PATCH 02/12] feat(paper): route /workspace/settings/appearance to AppearanceSettingsView --- frontend/taskdeck-web/src/router/index.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frontend/taskdeck-web/src/router/index.ts b/frontend/taskdeck-web/src/router/index.ts index 125c7480e..072cc8053 100644 --- a/frontend/taskdeck-web/src/router/index.ts +++ b/frontend/taskdeck-web/src/router/index.ts @@ -34,6 +34,7 @@ const ExportImportView = () => import('../views/ExportImportView.vue') const ArchiveView = () => import('../views/ArchiveView.vue') const NotificationInboxView = () => import('../views/NotificationInboxView.vue') const NotificationPreferencesView = () => import('../views/NotificationPreferencesView.vue') +const AppearanceSettingsView = () => import('../views/AppearanceSettingsView.vue') const InboxView = () => import('../views/InboxView.vue') const HomeView = () => import('../views/HomeView.vue') const TodayView = () => import('../views/TodayView.vue') @@ -276,6 +277,12 @@ const router = createRouter({ component: NotificationPreferencesView, meta: { requiresShell: true, breadcrumb: 'Preferences' }, }, + { + path: '/workspace/settings/appearance', + name: 'workspace-settings-appearance', + component: AppearanceSettingsView, + meta: { requiresShell: true, breadcrumb: 'Appearance' }, + }, { path: '/workspace/settings/api-keys', name: 'workspace-settings-api-keys', From 07debb23520d6eb0403eca8a0ae4fb0a482d1db4 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sat, 13 Jun 2026 18:01:46 +0100 Subject: [PATCH 03/12] feat(paper): link Appearance settings from the Legacy ShellSidebar --- .../taskdeck-web/src/components/shell/ShellSidebar.vue | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frontend/taskdeck-web/src/components/shell/ShellSidebar.vue b/frontend/taskdeck-web/src/components/shell/ShellSidebar.vue index ce93f2a65..1d27efda2 100644 --- a/frontend/taskdeck-web/src/components/shell/ShellSidebar.vue +++ b/frontend/taskdeck-web/src/components/shell/ShellSidebar.vue @@ -248,6 +248,16 @@ const navCatalog: NavItem[] = [ secondaryModes: ['guided', 'agent'], keywords: 'preferences notifications', }, + { + id: 'appearance', + label: 'Appearance', + icon: 'T', + path: '/workspace/settings/appearance', + flag: null, + primaryModes: ['workbench'], + secondaryModes: ['guided', 'agent'], + keywords: 'appearance theme paper night dark light obsidian legacy', + }, { id: 'access', label: 'Access', From 8b9769acdeaa908dbcee03f2340e002356368c33 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sat, 13 Jun 2026 18:01:46 +0100 Subject: [PATCH 04/12] feat(paper): link Appearance settings from the PaperSidebar meta nav --- frontend/taskdeck-web/src/components/paper/PaperSidebar.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/taskdeck-web/src/components/paper/PaperSidebar.vue b/frontend/taskdeck-web/src/components/paper/PaperSidebar.vue index f97668d66..8bd9534b8 100644 --- a/frontend/taskdeck-web/src/components/paper/PaperSidebar.vue +++ b/frontend/taskdeck-web/src/components/paper/PaperSidebar.vue @@ -92,6 +92,7 @@ const metaItems: PaperNavItem[] = [ { id: 'settings', label: 'Settings', glyph: 'S', path: '/workspace/settings/profile', flag: 'newAuth', workbenchBypassesFlag: true, keywords: 'settings profile password account' }, { id: 'api-keys', label: 'API Keys', glyph: 'K', path: '/workspace/settings/api-keys', keywords: 'api keys mcp tokens authentication' }, { id: 'preferences', label: 'Preferences', glyph: 'P', path: '/workspace/settings/preferences', keywords: 'preferences notifications' }, + { id: 'appearance', label: 'Appearance', glyph: 'T', path: '/workspace/settings/appearance', keywords: 'appearance theme paper night dark light obsidian legacy' }, { id: 'shortcuts', label: 'Shortcuts', glyph: '?', path: '#shortcuts' }, { id: 'logout', label: 'Logout', glyph: '→', path: '#logout' }, ] From 45245374a05369cc23a64365cc80e79c8b8cc3b4 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sat, 13 Jun 2026 18:01:47 +0100 Subject: [PATCH 05/12] test(paper): cover AppearanceSettingsView theme toggle (render, aria-pressed, setMode, auto, off-restores-legacy) --- .../views/AppearanceSettingsView.spec.ts | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 frontend/taskdeck-web/src/tests/views/AppearanceSettingsView.spec.ts diff --git a/frontend/taskdeck-web/src/tests/views/AppearanceSettingsView.spec.ts b/frontend/taskdeck-web/src/tests/views/AppearanceSettingsView.spec.ts new file mode 100644 index 000000000..67c0f5c4b --- /dev/null +++ b/frontend/taskdeck-web/src/tests/views/AppearanceSettingsView.spec.ts @@ -0,0 +1,80 @@ +import { beforeEach, afterEach, describe, expect, it } from 'vitest' +import { mount } from '@vue/test-utils' +import { setActivePinia, createPinia } from 'pinia' +import AppearanceSettingsView from '../../views/AppearanceSettingsView.vue' +import { usePaperThemeStore } from '../../store/paperThemeStore' + +const STORAGE_KEY = 'td.paper.mode' + +function segmentByLabel(wrapper: ReturnType, label: string) { + return wrapper + .findAll('.td-theme-segment') + .find((b) => b.text().includes(label)) +} + +describe('AppearanceSettingsView', () => { + beforeEach(() => { + setActivePinia(createPinia()) + window.localStorage.clear() + document.body.classList.remove('paper', 'paper-night') + }) + + afterEach(() => { + document.body.classList.remove('paper', 'paper-night') + }) + + it('renders all four theme options', () => { + const wrapper = mount(AppearanceSettingsView) + const labels = wrapper.findAll('.td-theme-segment').map((b) => b.text()) + expect(labels).toHaveLength(4) + expect(labels.some((l) => l.includes('Off (Legacy / Obsidian)'))).toBe(true) + expect(labels.some((l) => l.includes('Paper (Light)'))).toBe(true) + expect(labels.some((l) => l.includes('Paper Night (Dark)'))).toBe(true) + expect(labels.some((l) => l.includes('Auto (match system)'))).toBe(true) + }) + + it('reflects the current mode via aria-pressed (default off)', () => { + const wrapper = mount(AppearanceSettingsView) + expect(segmentByLabel(wrapper, 'Off (Legacy / Obsidian)')?.attributes('aria-pressed')).toBe('true') + expect(segmentByLabel(wrapper, 'Paper (Light)')?.attributes('aria-pressed')).toBe('false') + expect(segmentByLabel(wrapper, 'Auto (match system)')?.attributes('aria-pressed')).toBe('false') + }) + + it('selecting a mode calls setMode and persists to localStorage', async () => { + const wrapper = mount(AppearanceSettingsView) + const store = usePaperThemeStore() + + await segmentByLabel(wrapper, 'Paper Night (Dark)')?.trigger('click') + + expect(store.mode).toBe('paper-night') + expect(window.localStorage.getItem(STORAGE_KEY)).toBe('paper-night') + expect(document.body.classList.contains('paper-night')).toBe(true) + expect(segmentByLabel(wrapper, 'Paper Night (Dark)')?.attributes('aria-pressed')).toBe('true') + }) + + it('Auto stays Auto (does not collapse to the resolved class)', async () => { + const wrapper = mount(AppearanceSettingsView) + const store = usePaperThemeStore() + + await segmentByLabel(wrapper, 'Auto (match system)')?.trigger('click') + + expect(store.mode).toBe('auto') + expect(window.localStorage.getItem(STORAGE_KEY)).toBe('auto') + expect(segmentByLabel(wrapper, 'Auto (match system)')?.attributes('aria-pressed')).toBe('true') + }) + + it('Off restores Legacy (no paper body class, store off)', async () => { + const wrapper = mount(AppearanceSettingsView) + const store = usePaperThemeStore() + + await segmentByLabel(wrapper, 'Paper (Light)')?.trigger('click') + expect(store.isOn).toBe(true) + + await segmentByLabel(wrapper, 'Off (Legacy / Obsidian)')?.trigger('click') + + expect(store.mode).toBe('off') + expect(store.isOn).toBe(false) + expect(document.body.classList.contains('paper')).toBe(false) + expect(document.body.classList.contains('paper-night')).toBe(false) + }) +}) From cd43ba696c1ac3003371ccab4911274e9e915224 Mon Sep 17 00:00:00 2001 From: Chris0Jeky Date: Sat, 13 Jun 2026 18:12:54 +0100 Subject: [PATCH 06/12] =?UTF-8?q?fix(paper):=20a11y=20+=20clarity=20on=20A?= =?UTF-8?q?ppearanceSettingsView=20=E2=80=94=20aria-labelledby,=20data-mod?= =?UTF-8?q?e=20hook,=20Legacy-token=20note=20(review)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/views/AppearanceSettingsView.vue | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/frontend/taskdeck-web/src/views/AppearanceSettingsView.vue b/frontend/taskdeck-web/src/views/AppearanceSettingsView.vue index 13706fb54..72d8399e2 100644 --- a/frontend/taskdeck-web/src/views/AppearanceSettingsView.vue +++ b/frontend/taskdeck-web/src/views/AppearanceSettingsView.vue @@ -55,14 +55,21 @@ function selectMode(mode: PaperMode) {

-
Theme
-
+
Theme
+ +