From 2a549a91356cea6b3262f8ee3fdfd05946625e35 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 4 Jul 2026 16:13:26 +0200 Subject: [PATCH 1/4] feat(ui): replace custom toggle with native input switch polyfill Uses the new element, polyfilled via input-switch-polyfill for browsers that don't support it yet, kept live-synced with the accent color and color-mode changes. --- app/components/Settings/Toggle.client.vue | 42 ++------------------- app/plugins/input-switch-polyfill.client.ts | 26 +++++++++++++ nuxt.config.ts | 2 +- package.json | 1 + pnpm-lock.yaml | 8 ++++ 5 files changed, 39 insertions(+), 40 deletions(-) create mode 100644 app/plugins/input-switch-polyfill.client.ts diff --git a/app/components/Settings/Toggle.client.vue b/app/components/Settings/Toggle.client.vue index 29b07f6b62..9a4864b6fb 100644 --- a/app/components/Settings/Toggle.client.vue +++ b/app/components/Settings/Toggle.client.vue @@ -54,51 +54,15 @@ const id = useId() {{ label }}

{{ description }}

- - diff --git a/app/plugins/input-switch-polyfill.client.ts b/app/plugins/input-switch-polyfill.client.ts new file mode 100644 index 0000000000..46dc136dd4 --- /dev/null +++ b/app/plugins/input-switch-polyfill.client.ts @@ -0,0 +1,26 @@ +export default defineNuxtPlugin(async () => { + if (!('switch' in HTMLInputElement.prototype)) { + // @ts-expect-error input-switch-polyfill ships no types; imported for its side effect only + await import('input-switch-polyfill') + + // The polyfill reads `accent-color` once at upgrade time and freezes the + // resolved color as `--switch-accent` in each element's inline style. + // Re-sync whenever the accent or color-mode changes so switches react live. + const syncSwitchAccent = () => { + const switches = document.querySelectorAll('input.switch') + const [first] = switches + if (!first) return + // All switches share the same cascade; read from the first one. + const color = getComputedStyle(first).getPropertyValue('accent-color').trim() + if (!color || color === 'auto') return + for (const el of switches) { + el.style.setProperty('--switch-accent', color) + } + } + + new MutationObserver(syncSwitchAccent).observe(document.documentElement, { + attributes: true, + attributeFilter: ['style', 'class'], + }) + } +}) diff --git a/nuxt.config.ts b/nuxt.config.ts index 401dfd006e..ec29109b1a 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -33,7 +33,7 @@ export default defineNuxtConfig({ storageKey: 'npmx-color-mode', }, - css: ['~/assets/main.css'], + css: ['~/assets/main.css', 'input-switch-polyfill/input-switch-polyfill.css'], runtimeConfig: { sessionPassword: '', diff --git a/package.json b/package.json index a83f9ec1c9..a765e013d2 100644 --- a/package.json +++ b/package.json @@ -89,6 +89,7 @@ "focus-trap": "^8.0.0", "gray-matter": "4.0.3", "hls.js": "1.6.16", + "input-switch-polyfill": "1.12.0", "ipaddr.js": "2.3.0", "marked": "18.0.0", "module-replacements": "3.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ffe4c756c..6db97f9011 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -186,6 +186,9 @@ importers: hls.js: specifier: 1.6.16 version: 1.6.16 + input-switch-polyfill: + specifier: 1.12.0 + version: 1.12.0 ipaddr.js: specifier: 2.3.0 version: 2.3.0 @@ -8001,6 +8004,9 @@ packages: resolution: {integrity: sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + input-switch-polyfill@1.12.0: + resolution: {integrity: sha512-4IF+lwC4jjInFPWWjHDcVJWvWiTHm1RnGa+P9uLhOleal69NPbUnrcPHJJhuexyUqOnyA0QenXhtZNuZX4gqWA==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -19588,6 +19594,8 @@ snapshots: ini@4.1.1: {} + input-switch-polyfill@1.12.0: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 From 21f95803563e1ceb4fbb57d6cb0540af1598a022 Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Fri, 2 Oct 2026 11:11:30 +0200 Subject: [PATCH 2/4] chore: retrigger CI From 8f7b611976bf11ef937dbd5ac67af77b7bf51dee Mon Sep 17 00:00:00 2001 From: Felix Schneider <99918022+trueberryless@users.noreply.github.com> Date: Sun, 1 Nov 2026 15:06:37 +0100 Subject: [PATCH 3/4] fix(ui): respect reduced motion and load switch polyfill in parallel --- app/components/Settings/Toggle.client.vue | 9 ++++ app/plugins/input-switch-polyfill.client.ts | 48 +++++++++++---------- 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/app/components/Settings/Toggle.client.vue b/app/components/Settings/Toggle.client.vue index 9a4864b6fb..4750e77a62 100644 --- a/app/components/Settings/Toggle.client.vue +++ b/app/components/Settings/Toggle.client.vue @@ -66,3 +66,12 @@ const id = useId() {{ description }}

+ + diff --git a/app/plugins/input-switch-polyfill.client.ts b/app/plugins/input-switch-polyfill.client.ts index 46dc136dd4..1cb57ccc92 100644 --- a/app/plugins/input-switch-polyfill.client.ts +++ b/app/plugins/input-switch-polyfill.client.ts @@ -1,26 +1,30 @@ -export default defineNuxtPlugin(async () => { - if (!('switch' in HTMLInputElement.prototype)) { - // @ts-expect-error input-switch-polyfill ships no types; imported for its side effect only - await import('input-switch-polyfill') +export default defineNuxtPlugin({ + // Don't hold up hydration for a polyfill that only the settings page needs. + parallel: true, + async setup() { + if (!('switch' in HTMLInputElement.prototype)) { + // @ts-expect-error input-switch-polyfill ships no types; imported for its side effect only + await import('input-switch-polyfill') - // The polyfill reads `accent-color` once at upgrade time and freezes the - // resolved color as `--switch-accent` in each element's inline style. - // Re-sync whenever the accent or color-mode changes so switches react live. - const syncSwitchAccent = () => { - const switches = document.querySelectorAll('input.switch') - const [first] = switches - if (!first) return - // All switches share the same cascade; read from the first one. - const color = getComputedStyle(first).getPropertyValue('accent-color').trim() - if (!color || color === 'auto') return - for (const el of switches) { - el.style.setProperty('--switch-accent', color) + // The polyfill reads `accent-color` once at upgrade time and freezes the + // resolved color as `--switch-accent` in each element's inline style. + // Re-sync whenever the accent or color-mode changes so switches react live. + const syncSwitchAccent = () => { + const switches = document.querySelectorAll('input.switch') + const [first] = switches + if (!first) return + // All switches share the same cascade; read from the first one. + const color = getComputedStyle(first).getPropertyValue('accent-color').trim() + if (!color || color === 'auto') return + for (const el of switches) { + el.style.setProperty('--switch-accent', color) + } } - } - new MutationObserver(syncSwitchAccent).observe(document.documentElement, { - attributes: true, - attributeFilter: ['style', 'class'], - }) - } + new MutationObserver(syncSwitchAccent).observe(document.documentElement, { + attributes: true, + attributeFilter: ['style', 'class'], + }) + } + }, }) From a449b5eeff8326c2dc05f57adb36e6193ea4502b Mon Sep 17 00:00:00 2001 From: Thomas Steiner Date: Sat, 14 Nov 2026 12:34:21 +0100 Subject: [PATCH 4/4] fix(ui): add explicit switch role to native toggle input --- app/components/Settings/Toggle.client.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/app/components/Settings/Toggle.client.vue b/app/components/Settings/Toggle.client.vue index 4750e77a62..20b9fd0c12 100644 --- a/app/components/Settings/Toggle.client.vue +++ b/app/components/Settings/Toggle.client.vue @@ -56,6 +56,7 @@ const id = useId()