diff --git a/app/assets/main.css b/app/assets/main.css
index 7a33ba3fc3..85e8802a2c 100644
--- a/app/assets/main.css
+++ b/app/assets/main.css
@@ -7,17 +7,17 @@
: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);
--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);
@@ -43,11 +43,39 @@
--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='dark'][data-bg-theme='black'] {
+ --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: 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 +103,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);
+}
+
@media (prefers-contrast: more) {
:root[data-theme='dark'] {
/* text colors */
diff --git a/app/components/Settings/BgThemePicker.vue b/app/components/Settings/BgThemePicker.vue
new file mode 100644
index 0000000000..c4f39eb94a
--- /dev/null
+++ b/app/components/Settings/BgThemePicker.vue
@@ -0,0 +1,36 @@
+
+
+
+
+
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/composables/useSettings.ts b/app/composables/useSettings.ts
index 73445a7497..b0c2f6ff63 100644
--- a/app/composables/useSettings.ts
+++ b/app/composables/useSettings.ts
@@ -2,6 +2,9 @@ import type { RemovableRef } from '@vueuse/core'
import { useLocalStorage } from '@vueuse/core'
import { ACCENT_COLORS } from '#shared/utils/constants'
import type { LocaleObject } from '@nuxtjs/i18n'
+import { BACKGROUND_THEMES } from '#shared/utils/constants'
+
+type BackgroundThemeId = keyof typeof BACKGROUND_THEMES
type AccentColorId = keyof typeof ACCENT_COLORS
@@ -15,6 +18,8 @@ export interface AppSettings {
includeTypesInInstall: boolean
/** Accent color theme */
accentColorId: AccentColorId | null
+ /** Preferred background shade */
+ preferredBackgroundTheme: BackgroundThemeId | null
/** Hide platform-specific packages (e.g., @scope/pkg-linux-x64) from search results */
hidePlatformPackages: boolean
/** User-selected locale */
@@ -30,6 +35,7 @@ const DEFAULT_SETTINGS: AppSettings = {
accentColorId: null,
hidePlatformPackages: true,
selectedLocale: null,
+ preferredBackgroundTheme: null,
sidebar: {
collapsed: [],
},
@@ -93,3 +99,28 @@ export function useAccentColor() {
setAccentColor,
}
}
+
+export function useBackgroundTheme() {
+ const backgroundThemes = Object.entries(BACKGROUND_THEMES).map(([id, value]) => ({
+ id: id as BackgroundThemeId,
+ name: id,
+ value,
+ }))
+
+ const { settings } = useSettings()
+
+ function setBackgroundTheme(id: BackgroundThemeId | null) {
+ if (id) {
+ document.documentElement.dataset.bgTheme = id
+ } else {
+ document.documentElement.removeAttribute('data-bg-theme')
+ }
+ settings.value.preferredBackgroundTheme = id
+ }
+
+ return {
+ backgroundThemes,
+ selectedBackgroundTheme: computed(() => settings.value.preferredBackgroundTheme),
+ setBackgroundTheme,
+ }
+}
diff --git a/app/pages/settings.vue b/app/pages/settings.vue
index c49c0e7848..38bba087b9 100644
--- a/app/pages/settings.vue
+++ b/app/pages/settings.vue
@@ -97,6 +97,14 @@ const setLocale: typeof setNuxti18nLocale = locale => {
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([]) + }) + } + }) + } +})