From 627c819a2e09d71c2214b602f401296fb95fabc7 Mon Sep 17 00:00:00 2001 From: Vordgi Date: Sun, 1 Feb 2026 17:27:53 +0000 Subject: [PATCH 1/6] feat: add background theming --- app/assets/main.css | 58 +++++++++++++++++++---- app/components/Settings/BgThemePicker.vue | 37 +++++++++++++++ app/composables/useBackgroundTheme.ts | 35 ++++++++++++++ app/composables/useColors.ts | 2 +- app/pages/settings.vue | 8 ++++ nuxt.config.ts | 12 +++++ shared/utils/constants.ts | 7 +++ 7 files changed, 150 insertions(+), 9 deletions(-) create mode 100644 app/components/Settings/BgThemePicker.vue create mode 100644 app/composables/useBackgroundTheme.ts diff --git a/app/assets/main.css b/app/assets/main.css index 31a9a71880..eee1379259 100644 --- a/app/assets/main.css +++ b/app/assets/main.css @@ -7,10 +7,10 @@ :root[data-theme='dark'] { /* background colors */ - --bg: oklch(0.145 0 0); - --bg-subtle: oklch(0.178 0 0); - --bg-muted: oklch(0.218 0 0); - --bg-elevated: oklch(0.252 0 0); + --bg: var(--bg-color, oklch(0.145 0 0)); + --bg-subtle: var(--bg-subtle-color, oklch(0.178 0 0)); + --bg-muted: var(--bg-muted-color, oklch(0.218 0 0)); + --bg-elevated: var(--bg-elevated-color, oklch(0.252 0 0)); /* text colors */ --fg: oklch(0.985 0 0); @@ -43,11 +43,32 @@ --badge-pink: oklch(0.584 0.189 343); } +:root[data-theme='dark'][data-bg-theme='slate'] { + --bg: oklch(0.129 0.012 264.695); + --bg-subtle: oklch(0.159 0.022 262.421); + --bg-muted: oklch(0.204 0.033 261.234); + --bg-elevated: oklch(0.259 0.041 260.031); +} + +:root[data-theme='dark'][data-bg-theme='zinc'] { + --bg: oklch(0.141 0.005 285.823); + --bg-subtle: oklch(0.168 0.005 285.894); + --bg-muted: oklch(0.209 0.005 285.929); + --bg-elevated: oklch(0.256 0.006 286.033); +} + +:root[data-theme='dark'][data-bg-theme='stone'] { + --bg: oklch(0.147 0.004 49.25); + --bg-subtle: oklch(0.178 0.004 49.321); + --bg-muted: oklch(0.218 0.004 49.386); + --bg-elevated: oklch(0.252 0.007 34.298); +} + :root[data-theme='light'] { - --bg: oklch(1 0 0); - --bg-subtle: oklch(0.979 0.001 286.375); - --bg-muted: oklch(0.955 0 0); - --bg-elevated: oklch(0.94 0 0); + --bg: var(--bg-color, oklch(1 0 0)); + --bg-subtle: var(--bg-subtle-color, oklch(0.979 0.001 286.375)); + --bg-muted: var(--bg-muted-color, oklch(0.955 0 0)); + --bg-elevated: var(--bg-elevated-color, oklch(0.94 0 0)); --fg: oklch(0.145 0 0); --fg-muted: oklch(0.439 0 0); @@ -75,6 +96,27 @@ --badge-cyan: oklch(0.571 0.181 210); } +:root[data-theme='light'][data-bg-theme='slate'] { + --bg: oklch(1 0 0); + --bg-subtle: oklch(0.982 0.006 264.62); + --bg-muted: oklch(0.96 0.041 261.234); + --bg-elevated: oklch(0.943 0.013 255.52); +} + +:root[data-theme='light'][data-bg-theme='zinc'] { + --bg: oklch(1 0 0); + --bg-subtle: oklch(0.979 0.004 286.53); + --bg-muted: oklch(0.958 0.004 286.39); + --bg-elevated: oklch(0.939 0.004 286.32); +} + +:root[data-theme='light'][data-bg-theme='stone'] { + --bg: oklch(1 0 0); + --bg-subtle: oklch(0.979 0.005 48.762); + --bg-muted: oklch(0.958 0.005 48.743); + --bg-elevated: oklch(0.943 0.005 48.731); +} + html { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; diff --git a/app/components/Settings/BgThemePicker.vue b/app/components/Settings/BgThemePicker.vue new file mode 100644 index 0000000000..0a31cd737d --- /dev/null +++ b/app/components/Settings/BgThemePicker.vue @@ -0,0 +1,37 @@ + + + diff --git a/app/composables/useBackgroundTheme.ts b/app/composables/useBackgroundTheme.ts new file mode 100644 index 0000000000..e8c0a0e0d4 --- /dev/null +++ b/app/composables/useBackgroundTheme.ts @@ -0,0 +1,35 @@ +import type { RemovableRef } from '@vueuse/core' +import { useLocalStorage } from '@vueuse/core' +import { BACKGROUND_THEMES } from '#shared/utils/constants' + +type BackgroundThemeId = keyof typeof BACKGROUND_THEMES + +export const BACKGROUND_THEME_STORAGE_KEY = 'npmx-background-theme' + +const backgroundThemeRef: RemovableRef = useLocalStorage( + BACKGROUND_THEME_STORAGE_KEY, + null, +) + +export function useBackgroundTheme() { + const backgroundThemes = Object.entries(BACKGROUND_THEMES).map(([id, value]) => ({ + id: id as BackgroundThemeId, + name: id, + value, + })) + + function setBackgroundTheme(id: BackgroundThemeId | null) { + if (id) { + document.documentElement.dataset.bgTheme = id + } else { + document.documentElement.removeAttribute('data-bg-theme') + } + backgroundThemeRef.value = id + } + + return { + backgroundThemes, + selectedBackgroundTheme: computed(() => backgroundThemeRef.value), + setBackgroundTheme, + } +} diff --git a/app/composables/useColors.ts b/app/composables/useColors.ts index 9cdb77c07a..3b66e25fd1 100644 --- a/app/composables/useColors.ts +++ b/app/composables/useColors.ts @@ -81,7 +81,7 @@ export function useCssVariables( if (options.watchHtmlAttributes && isClientSupported.value) { useMutationObserver(document.documentElement, () => void colors.value, { attributes: true, - attributeFilter: ['class', 'style', 'data-theme'], + attributeFilter: ['class', 'style', 'data-theme', 'data-bg-theme'], }) } diff --git a/app/pages/settings.vue b/app/pages/settings.vue index ceca5c04b2..66f7c98c9a 100644 --- a/app/pages/settings.vue +++ b/app/pages/settings.vue @@ -97,6 +97,14 @@ const setLocale: typeof setNuxti18nLocale = locale => { + + +
+ + {{ $t('settings.background_themes') }} + + +
diff --git a/nuxt.config.ts b/nuxt.config.ts index a21fc498fa..e9a8e964c6 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -73,6 +73,18 @@ export default defineNuxtConfig({ href: '/opensearch.xml', }, ], + script: [ + { + innerHTML: ` + (function () { + const preferredBackgroundTheme = localStorage.getItem('npmx-background-theme') + if (preferredBackgroundTheme) { + document.documentElement.dataset.bgTheme = preferredBackgroundTheme + } + })() + `, + }, + ], }, }, diff --git a/shared/utils/constants.ts b/shared/utils/constants.ts index d2292de6a1..958e3369f1 100644 --- a/shared/utils/constants.ts +++ b/shared/utils/constants.ts @@ -34,3 +34,10 @@ export const ACCENT_COLORS = { violet: 'oklch(0.714 0.148 286.067)', coral: 'oklch(0.704 0.177 14.75)', } as const + +export const BACKGROUND_THEMES = { + neutral: 'oklch(0.555 0 0)', + stone: 'oklch(0.555 0.013 58.123)', + zinc: 'oklch(0.555 0.016 285.931)', + slate: 'oklch(0.555 0.046 257.407)', +} as const From 9fb44c467d81dd1ed7ec88fa7144ff34a99ccbaf Mon Sep 17 00:00:00 2001 From: Vordgi Date: Sun, 1 Feb 2026 18:37:54 +0000 Subject: [PATCH 2/6] feat: add a11y tests for background theming --- test/nuxt/a11y.spec.ts | 133 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/test/nuxt/a11y.spec.ts b/test/nuxt/a11y.spec.ts index 8225152585..c201982088 100644 --- a/test/nuxt/a11y.spec.ts +++ b/test/nuxt/a11y.spec.ts @@ -106,6 +106,7 @@ import { ProvenanceBadge, Readme, SettingsAccentColorPicker, + SettingsBgThemePicker, SettingsToggle, TerminalExecute, TerminalInstall, @@ -1413,6 +1414,14 @@ describe('component accessibility audits', () => { }) }) + describe('SettingsBgThemePicker', () => { + it('should have no accessibility violations', async () => { + const component = await mountSuspended(SettingsBgThemePicker) + const results = await runAxe(component) + expect(results.violations).toEqual([]) + }) + }) + describe('TooltipBase', () => { it('should have no accessibility violations when hidden', async () => { const component = await mountSuspended(TooltipBase, { @@ -1788,3 +1797,127 @@ describe('component accessibility audits', () => { }) }) }) + +describe('background theme accessibility', () => { + const pairs = [ + ['light', 'neutral'], + ['dark', 'neutral'], + ['light', 'stone'], + ['dark', 'stone'], + ['light', 'zinc'], + ['dark', 'zinc'], + ['light', 'slate'], + ['dark', 'slate'], + ] as const + + function applyTheme(colorMode: string, bgTheme: string | null) { + document.documentElement.dataset.theme = colorMode + document.documentElement.classList.add(colorMode) + if (bgTheme) document.documentElement.dataset.bgTheme = bgTheme + } + + afterEach(() => { + document.documentElement.removeAttribute('data-theme') + document.documentElement.removeAttribute('data-bg-theme') + document.documentElement.classList.remove('light', 'dark') + }) + + const packageResult = { + package: { + name: 'vue', + version: '3.5.0', + description: 'Framework', + date: '2024-01-15T00:00:00.000Z', + keywords: [], + links: {}, + publisher: { username: 'evan' }, + }, + score: { final: 0.9, detail: { quality: 0.9, popularity: 0.9, maintenance: 0.9 } }, + searchScore: 100000, + } + + const components = [ + { name: 'AppHeader', mount: () => mountSuspended(AppHeader) }, + { name: 'AppFooter', mount: () => mountSuspended(AppFooter) }, + { name: 'HeaderSearchBox', mount: () => mountSuspended(HeaderSearchBox) }, + { + name: 'LoadingSpinner', + mount: () => mountSuspended(LoadingSpinner, { props: { text: 'Loading...' } }), + }, + { + name: 'SettingsToggle', + mount: () => + mountSuspended(SettingsToggle, { props: { label: 'Feature', description: 'Desc' } }), + }, + { name: 'SettingsBgThemePicker', mount: () => mountSuspended(SettingsBgThemePicker) }, + { + name: 'ProvenanceBadge', + mount: () => + mountSuspended(ProvenanceBadge, { + props: { provider: 'github', packageName: 'vue', version: '3.0.0' }, + }), + }, + { + name: 'TerminalInstall', + mount: () => mountSuspended(TerminalInstall, { props: { packageName: 'vue' } }), + }, + { + name: 'LicenseDisplay', + mount: () => mountSuspended(LicenseDisplay, { props: { license: 'MIT' } }), + }, + { + name: 'DateTime', + mount: () => mountSuspended(DateTime, { props: { datetime: '2024-01-15T12:00:00.000Z' } }), + }, + { + name: 'ViewModeToggle', + mount: () => mountSuspended(ViewModeToggle, { props: { modelValue: 'cards' } }), + }, + { + name: 'TooltipApp', + mount: () => + mountSuspended(TooltipApp, { + props: { text: 'Tooltip' }, + slots: { default: '' }, + }), + }, + { + name: 'CollapsibleSection', + mount: () => + mountSuspended(CollapsibleSection, { + props: { title: 'Title', id: 'section' }, + slots: { default: '

Content

' }, + }), + }, + { + name: 'FilterChips', + mount: () => + mountSuspended(FilterChips, { + props: { + chips: [{ id: 'text', type: 'text', label: 'Search', value: 'react' }] as FilterChip[], + }, + }), + }, + { + name: 'PackageCard', + mount: () => mountSuspended(PackageCard, { props: { result: packageResult } }), + }, + { + name: 'PackageList', + mount: () => mountSuspended(PackageList, { props: { results: [packageResult] } }), + }, + ] + + for (const { name, mount } of components) { + describe(`${name} colors`, () => { + for (const [colorMode, bgTheme] of pairs) { + it(`${colorMode}/${bgTheme}`, async () => { + applyTheme(colorMode, bgTheme) + const results = await runAxe(await mount()) + await new Promise(resolve => setTimeout(resolve, 2000)) + expect(results.violations).toEqual([]) + }) + } + }) + } +}) From 3426192a31f8b1e1399eb301e04b9cf9217c505f Mon Sep 17 00:00:00 2001 From: Vordgi Date: Sun, 1 Feb 2026 18:59:08 +0000 Subject: [PATCH 3/6] feat: add contrast bg theme for black mode --- app/assets/main.css | 7 +++++++ shared/utils/constants.ts | 1 + test/nuxt/a11y.spec.ts | 2 ++ 3 files changed, 10 insertions(+) diff --git a/app/assets/main.css b/app/assets/main.css index eee1379259..32f78f9828 100644 --- a/app/assets/main.css +++ b/app/assets/main.css @@ -64,6 +64,13 @@ --bg-elevated: oklch(0.252 0.007 34.298); } +:root[data-theme='dark'][data-bg-theme='contrast'] { + --bg: oklch(0 0 0); + --bg-subtle: oklch(0.148 0 0); + --bg-muted: oklch(0.204 0 0); + --bg-elevated: oklch(0.264 0 0); +} + :root[data-theme='light'] { --bg: var(--bg-color, oklch(1 0 0)); --bg-subtle: var(--bg-subtle-color, oklch(0.979 0.001 286.375)); diff --git a/shared/utils/constants.ts b/shared/utils/constants.ts index 958e3369f1..e72d5f51ab 100644 --- a/shared/utils/constants.ts +++ b/shared/utils/constants.ts @@ -40,4 +40,5 @@ export const BACKGROUND_THEMES = { stone: 'oklch(0.555 0.013 58.123)', zinc: 'oklch(0.555 0.016 285.931)', slate: 'oklch(0.555 0.046 257.407)', + contrast: 'oklch(0.4 0 0)', } as const diff --git a/test/nuxt/a11y.spec.ts b/test/nuxt/a11y.spec.ts index c201982088..2588566364 100644 --- a/test/nuxt/a11y.spec.ts +++ b/test/nuxt/a11y.spec.ts @@ -1808,6 +1808,8 @@ describe('background theme accessibility', () => { ['dark', 'zinc'], ['light', 'slate'], ['dark', 'slate'], + ['light', 'contrast'], + ['dark', 'contrast'], ] as const function applyTheme(colorMode: string, bgTheme: string | null) { From 9a09c7e3bff2578f3ffd2f51d11b3ce8e6825068 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 2 Feb 2026 10:49:00 +0000 Subject: [PATCH 4/6] fix: rename to black to avoid confusion with perfers-contrast settings --- app/assets/main.css | 4 ++-- shared/utils/constants.ts | 2 +- test/nuxt/a11y.spec.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/main.css b/app/assets/main.css index 75c88fe0c5..85e8802a2c 100644 --- a/app/assets/main.css +++ b/app/assets/main.css @@ -17,7 +17,7 @@ --fg-muted: oklch(0.709 0 0); --fg-subtle: oklch(0.633 0 0); - /* border, seperator colors */ + /* border, separator colors */ --border: oklch(0.269 0 0); --border-subtle: oklch(0.239 0 0); --border-hover: oklch(0.371 0 0); @@ -64,7 +64,7 @@ --bg-elevated: oklch(0.252 0.007 34.298); } -:root[data-theme='dark'][data-bg-theme='contrast'] { +:root[data-theme='dark'][data-bg-theme='black'] { --bg: oklch(0 0 0); --bg-subtle: oklch(0.148 0 0); --bg-muted: oklch(0.204 0 0); diff --git a/shared/utils/constants.ts b/shared/utils/constants.ts index 3d07e8d8e0..5823835083 100644 --- a/shared/utils/constants.ts +++ b/shared/utils/constants.ts @@ -45,5 +45,5 @@ export const BACKGROUND_THEMES = { stone: 'oklch(0.555 0.013 58.123)', zinc: 'oklch(0.555 0.016 285.931)', slate: 'oklch(0.555 0.046 257.407)', - contrast: 'oklch(0.4 0 0)', + black: 'oklch(0.4 0 0)', } as const diff --git a/test/nuxt/a11y.spec.ts b/test/nuxt/a11y.spec.ts index 8f52e8e140..564296b5df 100644 --- a/test/nuxt/a11y.spec.ts +++ b/test/nuxt/a11y.spec.ts @@ -1848,8 +1848,8 @@ describe('background theme accessibility', () => { ['dark', 'zinc'], ['light', 'slate'], ['dark', 'slate'], - ['light', 'contrast'], - ['dark', 'contrast'], + ['light', 'black'], + ['dark', 'black'], ] as const function applyTheme(colorMode: string, bgTheme: string | null) { From c937b8c2fe604e60fd5a8d2bbbbc8be0b12fc99a Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 2 Feb 2026 10:50:28 +0000 Subject: [PATCH 5/6] chore: add suggested setting name --- i18n/locales/en.json | 3 ++- lunaria/files/en-US.json | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 9330ad174f..25a7c1fb87 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -77,7 +77,8 @@ "help_translate": "Help translate npmx", "accent_colors": "Accent colors", "clear_accent": "Clear accent color", - "translation_progress": "Translation progress" + "translation_progress": "Translation progress", + "background_themes": "Background shade" }, "i18n": { "missing_keys": "{count} missing translation | {count} missing translations", diff --git a/lunaria/files/en-US.json b/lunaria/files/en-US.json index 9330ad174f..25a7c1fb87 100644 --- a/lunaria/files/en-US.json +++ b/lunaria/files/en-US.json @@ -77,7 +77,8 @@ "help_translate": "Help translate npmx", "accent_colors": "Accent colors", "clear_accent": "Clear accent color", - "translation_progress": "Translation progress" + "translation_progress": "Translation progress", + "background_themes": "Background shade" }, "i18n": { "missing_keys": "{count} missing translation | {count} missing translations", From 04cf466d3b90728020020822dff0d2ae645c4839 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Mon, 2 Feb 2026 10:57:48 +0000 Subject: [PATCH 6/6] refactor: combine with other settings --- app/components/Settings/BgThemePicker.vue | 5 ++-- app/composables/useBackgroundTheme.ts | 35 ----------------------- app/composables/useSettings.ts | 31 ++++++++++++++++++++ app/utils/prehydrate.ts | 6 ++++ nuxt.config.ts | 12 -------- 5 files changed, 39 insertions(+), 50 deletions(-) delete mode 100644 app/composables/useBackgroundTheme.ts diff --git a/app/components/Settings/BgThemePicker.vue b/app/components/Settings/BgThemePicker.vue index 0a31cd737d..c4f39eb94a 100644 --- a/app/components/Settings/BgThemePicker.vue +++ b/app/components/Settings/BgThemePicker.vue @@ -1,10 +1,9 @@