diff --git a/app/app.vue b/app/app.vue index 2f5ff3bb5a..7f4f1244d5 100644 --- a/app/app.vue +++ b/app/app.vue @@ -4,6 +4,9 @@ import { useEventListener } from '@vueuse/core' const route = useRoute() const router = useRouter() +// Initialize accent color before hydration to prevent flash +initAccentOnPrehydrate() + const isHomepage = computed(() => route.path === '/') useHead({ diff --git a/app/components/AccentColorPicker.vue b/app/components/AccentColorPicker.vue new file mode 100644 index 0000000000..8a209732a3 --- /dev/null +++ b/app/components/AccentColorPicker.vue @@ -0,0 +1,29 @@ + + + diff --git a/app/components/AppHeader.vue b/app/components/AppHeader.vue index 7c1468b631..e3da8fe7bb 100644 --- a/app/components/AppHeader.vue +++ b/app/components/AppHeader.vue @@ -24,7 +24,7 @@ const { isConnected, npmUser } = useConnector() :aria-label="$t('header.home')" class="header-logo font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring rounded" > - ./npmx + ./npmx diff --git a/app/components/SettingsMenu.vue b/app/components/SettingsMenu.vue index 9a857e81bb..3936992678 100644 --- a/app/components/SettingsMenu.vue +++ b/app/components/SettingsMenu.vue @@ -163,6 +163,10 @@ onKeyStroke(',', e => { + +
+ +
diff --git a/app/composables/useSettings.ts b/app/composables/useSettings.ts index 67328b29e3..ac615a9fa7 100644 --- a/app/composables/useSettings.ts +++ b/app/composables/useSettings.ts @@ -1,5 +1,8 @@ import type { RemovableRef } from '@vueuse/core' import { useLocalStorage } from '@vueuse/core' +import { ACCENT_COLORS } from '#shared/utils/constants' + +type AccentColorId = keyof typeof ACCENT_COLORS /** * Application settings stored in localStorage @@ -9,11 +12,14 @@ export interface AppSettings { relativeDates: boolean /** Include @types/* package in install command for packages without built-in types */ includeTypesInInstall: boolean + /** Accent color theme */ + accentColorId: AccentColorId | null } const DEFAULT_SETTINGS: AppSettings = { relativeDates: false, includeTypesInInstall: true, + accentColorId: null, } const STORAGE_KEY = 'npmx-settings' @@ -45,3 +51,56 @@ export function useRelativeDates() { const { settings } = useSettings() return computed(() => settings.value.relativeDates) } + +/** + * Composable for managing accent color. + */ +export function useAccentColor() { + const { settings } = useSettings() + + const accentColors = Object.entries(ACCENT_COLORS).map(([id, value]) => ({ + id: id as AccentColorId, + name: id, + value, + })) + + function setAccentColor(id: AccentColorId | null) { + const color = id ? ACCENT_COLORS[id] : null + if (color) { + document.documentElement.style.setProperty('--accent-color', color) + } else { + document.documentElement.style.removeProperty('--accent-color') + } + settings.value.accentColorId = id + } + + return { + accentColors, + selectedAccentColor: computed(() => settings.value.accentColorId), + setAccentColor, + } +} + +/** + * Applies accent color before hydration to prevent flash of default color. + * Call this from app.vue to ensure accent color is applied on every page. + */ +export function initAccentOnPrehydrate() { + // Callback is stringified by Nuxt - external variables won't be available. + // Colors must be hardcoded since ACCENT_COLORS can't be referenced. + onPrehydrate(() => { + const colors: Record = { + rose: '#e9aeba', + amber: '#fbbf24', + emerald: '#34d399', + sky: '#38bdf8', + violet: '#a78bfa', + coral: '#fb7185', + } + const settings = JSON.parse(localStorage.getItem('npmx-settings') || '{}') + const color = settings.accentColorId ? colors[settings.accentColorId as AccentColorId] : null + if (color) { + document.documentElement.style.setProperty('--accent-color', color) + } + }) +} diff --git a/app/pages/index.vue b/app/pages/index.vue index bbc7a93a76..3dba648d7a 100644 --- a/app/pages/index.vue +++ b/app/pages/index.vue @@ -27,7 +27,7 @@ defineOgImageComponent('Default')

- ./npmx + ./npmx