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 @@
+
+
+
+
+
Appearance
+
+ Choose how Taskdeck looks. Paper is the canonical theme; Off keeps the original Legacy (Obsidian) shell.
+
+
+
+ Theme
+
+
+
+ {{ activeHint }}
+
+
+
+
+
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
+
+