Skip to content

Commit bf03fba

Browse files
alexdlndanielroe
andauthored
feat: add background theming (#663)
Co-authored-by: Daniel Roe <daniel@roe.dev>
1 parent 1a48cfe commit bf03fba

File tree

10 files changed

+287
-12
lines changed

10 files changed

+287
-12
lines changed

app/assets/main.css

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,17 @@
77

88
:root[data-theme='dark'] {
99
/* background colors */
10-
--bg: oklch(0.145 0 0);
11-
--bg-subtle: oklch(0.178 0 0);
12-
--bg-muted: oklch(0.218 0 0);
13-
--bg-elevated: oklch(0.252 0 0);
10+
--bg: var(--bg-color, oklch(0.145 0 0));
11+
--bg-subtle: var(--bg-subtle-color, oklch(0.178 0 0));
12+
--bg-muted: var(--bg-muted-color, oklch(0.218 0 0));
13+
--bg-elevated: var(--bg-elevated-color, oklch(0.252 0 0));
1414

1515
/* text colors */
1616
--fg: oklch(0.985 0 0);
1717
--fg-muted: oklch(0.709 0 0);
1818
--fg-subtle: oklch(0.633 0 0);
1919

20-
/* border, seperator colors */
20+
/* border, separator colors */
2121
--border: oklch(0.269 0 0);
2222
--border-subtle: oklch(0.239 0 0);
2323
--border-hover: oklch(0.371 0 0);
@@ -43,11 +43,39 @@
4343
--badge-pink: oklch(0.584 0.189 343);
4444
}
4545

46+
:root[data-theme='dark'][data-bg-theme='slate'] {
47+
--bg: oklch(0.129 0.012 264.695);
48+
--bg-subtle: oklch(0.159 0.022 262.421);
49+
--bg-muted: oklch(0.204 0.033 261.234);
50+
--bg-elevated: oklch(0.259 0.041 260.031);
51+
}
52+
53+
:root[data-theme='dark'][data-bg-theme='zinc'] {
54+
--bg: oklch(0.141 0.005 285.823);
55+
--bg-subtle: oklch(0.168 0.005 285.894);
56+
--bg-muted: oklch(0.209 0.005 285.929);
57+
--bg-elevated: oklch(0.256 0.006 286.033);
58+
}
59+
60+
:root[data-theme='dark'][data-bg-theme='stone'] {
61+
--bg: oklch(0.147 0.004 49.25);
62+
--bg-subtle: oklch(0.178 0.004 49.321);
63+
--bg-muted: oklch(0.218 0.004 49.386);
64+
--bg-elevated: oklch(0.252 0.007 34.298);
65+
}
66+
67+
:root[data-theme='dark'][data-bg-theme='black'] {
68+
--bg: oklch(0 0 0);
69+
--bg-subtle: oklch(0.148 0 0);
70+
--bg-muted: oklch(0.204 0 0);
71+
--bg-elevated: oklch(0.264 0 0);
72+
}
73+
4674
:root[data-theme='light'] {
47-
--bg: oklch(1 0 0);
48-
--bg-subtle: oklch(0.979 0.001 286.375);
49-
--bg-muted: oklch(0.955 0 0);
50-
--bg-elevated: oklch(0.94 0 0);
75+
--bg: var(--bg-color, oklch(1 0 0));
76+
--bg-subtle: var(--bg-subtle-color, oklch(0.979 0.001 286.375));
77+
--bg-muted: var(--bg-muted-color, oklch(0.955 0 0));
78+
--bg-elevated: var(--bg-elevated-color, oklch(0.94 0 0));
5179

5280
--fg: oklch(0.145 0 0);
5381
--fg-muted: oklch(0.439 0 0);
@@ -75,6 +103,27 @@
75103
--badge-cyan: oklch(0.571 0.181 210);
76104
}
77105

106+
:root[data-theme='light'][data-bg-theme='slate'] {
107+
--bg: oklch(1 0 0);
108+
--bg-subtle: oklch(0.982 0.006 264.62);
109+
--bg-muted: oklch(0.96 0.041 261.234);
110+
--bg-elevated: oklch(0.943 0.013 255.52);
111+
}
112+
113+
:root[data-theme='light'][data-bg-theme='zinc'] {
114+
--bg: oklch(1 0 0);
115+
--bg-subtle: oklch(0.979 0.004 286.53);
116+
--bg-muted: oklch(0.958 0.004 286.39);
117+
--bg-elevated: oklch(0.939 0.004 286.32);
118+
}
119+
120+
:root[data-theme='light'][data-bg-theme='stone'] {
121+
--bg: oklch(1 0 0);
122+
--bg-subtle: oklch(0.979 0.005 48.762);
123+
--bg-muted: oklch(0.958 0.005 48.743);
124+
--bg-elevated: oklch(0.943 0.005 48.731);
125+
}
126+
78127
@media (prefers-contrast: more) {
79128
:root[data-theme='dark'] {
80129
/* text colors */
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<script setup lang="ts">
2+
const { backgroundThemes, selectedBackgroundTheme, setBackgroundTheme } = useBackgroundTheme()
3+
4+
onPrehydrate(el => {
5+
const settings = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
6+
const id = settings.preferredBackgroundTheme
7+
if (id) {
8+
const input = el.querySelector<HTMLInputElement>(`input[value="${id || 'neutral'}"]`)
9+
if (input) {
10+
input.checked = true
11+
}
12+
}
13+
})
14+
</script>
15+
16+
<template>
17+
<fieldset class="flex items-center gap-4">
18+
<legend class="sr-only">{{ $t('settings.background_themes') }}</legend>
19+
<label
20+
v-for="theme in backgroundThemes"
21+
:key="theme.id"
22+
class="size-6 rounded-full transition-transform duration-150 motion-safe:hover:scale-110 cursor-pointer has-[:checked]:(ring-2 ring-fg ring-offset-2 ring-offset-bg-subtle) has-[:focus-visible]:(ring-2 ring-fg ring-offset-2 ring-offset-bg-subtle)"
23+
:style="{ backgroundColor: theme.value }"
24+
>
25+
<input
26+
type="radio"
27+
name="background-theme"
28+
class="sr-only"
29+
:value="theme.id"
30+
:checked="selectedBackgroundTheme === theme.id"
31+
:aria-label="theme.name"
32+
@change="setBackgroundTheme(theme.id)"
33+
/>
34+
</label>
35+
</fieldset>
36+
</template>

app/composables/useColors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export function useCssVariables(
8181
if (options.watchHtmlAttributes && isClientSupported.value) {
8282
useMutationObserver(document.documentElement, () => void colors.value, {
8383
attributes: true,
84-
attributeFilter: ['class', 'style', 'data-theme'],
84+
attributeFilter: ['class', 'style', 'data-theme', 'data-bg-theme'],
8585
})
8686
}
8787

app/composables/useSettings.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import type { RemovableRef } from '@vueuse/core'
22
import { useLocalStorage } from '@vueuse/core'
33
import { ACCENT_COLORS } from '#shared/utils/constants'
44
import type { LocaleObject } from '@nuxtjs/i18n'
5+
import { BACKGROUND_THEMES } from '#shared/utils/constants'
6+
7+
type BackgroundThemeId = keyof typeof BACKGROUND_THEMES
58

69
type AccentColorId = keyof typeof ACCENT_COLORS
710

@@ -15,6 +18,8 @@ export interface AppSettings {
1518
includeTypesInInstall: boolean
1619
/** Accent color theme */
1720
accentColorId: AccentColorId | null
21+
/** Preferred background shade */
22+
preferredBackgroundTheme: BackgroundThemeId | null
1823
/** Hide platform-specific packages (e.g., @scope/pkg-linux-x64) from search results */
1924
hidePlatformPackages: boolean
2025
/** User-selected locale */
@@ -30,6 +35,7 @@ const DEFAULT_SETTINGS: AppSettings = {
3035
accentColorId: null,
3136
hidePlatformPackages: true,
3237
selectedLocale: null,
38+
preferredBackgroundTheme: null,
3339
sidebar: {
3440
collapsed: [],
3541
},
@@ -93,3 +99,28 @@ export function useAccentColor() {
9399
setAccentColor,
94100
}
95101
}
102+
103+
export function useBackgroundTheme() {
104+
const backgroundThemes = Object.entries(BACKGROUND_THEMES).map(([id, value]) => ({
105+
id: id as BackgroundThemeId,
106+
name: id,
107+
value,
108+
}))
109+
110+
const { settings } = useSettings()
111+
112+
function setBackgroundTheme(id: BackgroundThemeId | null) {
113+
if (id) {
114+
document.documentElement.dataset.bgTheme = id
115+
} else {
116+
document.documentElement.removeAttribute('data-bg-theme')
117+
}
118+
settings.value.preferredBackgroundTheme = id
119+
}
120+
121+
return {
122+
backgroundThemes,
123+
selectedBackgroundTheme: computed(() => settings.value.preferredBackgroundTheme),
124+
setBackgroundTheme,
125+
}
126+
}

app/pages/settings.vue

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,14 @@ const setLocale: typeof setNuxti18nLocale = locale => {
9797
</span>
9898
<SettingsAccentColorPicker />
9999
</div>
100+
101+
<!-- Background themes -->
102+
<div class="space-y-3">
103+
<span class="block text-sm text-fg font-medium">
104+
{{ $t('settings.background_themes') }}
105+
</span>
106+
<SettingsBgThemePicker />
107+
</div>
100108
</div>
101109
</section>
102110

app/utils/prehydrate.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ export function initPreferencesOnPrehydrate() {
3535
document.documentElement.style.setProperty('--accent-color', color)
3636
}
3737

38+
// Apply background accent
39+
const preferredBackgroundTheme = settings.preferredBackgroundTheme
40+
if (preferredBackgroundTheme) {
41+
document.documentElement.dataset.bgTheme = preferredBackgroundTheme
42+
}
43+
3844
// Read and apply package manager preference
3945
const storedPM = localStorage.getItem('npmx-pm')
4046
// Parse the stored value (it's stored as a JSON string by useLocalStorage)

i18n/locales/en.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@
7777
"help_translate": "Help translate npmx",
7878
"accent_colors": "Accent colors",
7979
"clear_accent": "Clear accent color",
80-
"translation_progress": "Translation progress"
80+
"translation_progress": "Translation progress",
81+
"background_themes": "Background shade"
8182
},
8283
"i18n": {
8384
"missing_keys": "{count} missing translation | {count} missing translations",

lunaria/files/en-US.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@
7777
"help_translate": "Help translate npmx",
7878
"accent_colors": "Accent colors",
7979
"clear_accent": "Clear accent color",
80-
"translation_progress": "Translation progress"
80+
"translation_progress": "Translation progress",
81+
"background_themes": "Background shade"
8182
},
8283
"i18n": {
8384
"missing_keys": "{count} missing translation | {count} missing translations",

shared/utils/constants.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,11 @@ export const ACCENT_COLORS = {
3939
violet: 'oklch(0.714 0.148 286.067)',
4040
coral: 'oklch(0.704 0.177 14.75)',
4141
} as const
42+
43+
export const BACKGROUND_THEMES = {
44+
neutral: 'oklch(0.555 0 0)',
45+
stone: 'oklch(0.555 0.013 58.123)',
46+
zinc: 'oklch(0.555 0.016 285.931)',
47+
slate: 'oklch(0.555 0.046 257.407)',
48+
black: 'oklch(0.4 0 0)',
49+
} as const

0 commit comments

Comments
 (0)