From 2c22012f1f5a396f2510f6175f397d93ce89b6d9 Mon Sep 17 00:00:00 2001 From: Adebesin Tolulope Date: Thu, 5 Mar 2026 22:33:45 +0100 Subject: [PATCH 1/2] chore: fix use settings --- app/composables/useSettings.ts | 62 ++++++++++++++++++++++++++++------ app/utils/prehydrate.ts | 5 +++ 2 files changed, 57 insertions(+), 10 deletions(-) diff --git a/app/composables/useSettings.ts b/app/composables/useSettings.ts index 5e45b4218d..ae9f6da696 100644 --- a/app/composables/useSettings.ts +++ b/app/composables/useSettings.ts @@ -1,5 +1,3 @@ -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' @@ -72,22 +70,66 @@ const DEFAULT_SETTINGS: AppSettings = { const STORAGE_KEY = 'npmx-settings' -// Shared settings instance (singleton per app) -let settingsRef: RemovableRef | null = null +/** + * Read settings from localStorage and merge with defaults. + */ +function readFromLocalStorage(): AppSettings { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (raw) { + const stored = JSON.parse(raw) + return { + ...DEFAULT_SETTINGS, + ...stored, + connector: { ...DEFAULT_SETTINGS.connector, ...stored.connector }, + sidebar: { ...DEFAULT_SETTINGS.sidebar, ...stored.sidebar }, + chartFilter: { ...DEFAULT_SETTINGS.chartFilter, ...stored.chartFilter }, + } + } + } catch {} + return { ...DEFAULT_SETTINGS } +} + +let syncInitialized = false /** - * Composable for managing application settings with localStorage persistence. - * Settings are shared across all components that use this composable. + * Composable for managing application settings. + * + * Uses useState for SSR-safe hydration (server and client agree on initial + * values during hydration) and syncs with localStorage on the client. + * The onPrehydrate script in prehydrate.ts handles DOM-level patches + * (accent color, bg theme, collapsed sections, etc.) to prevent visual + * flash before hydration. */ export function useSettings() { - if (!settingsRef) { - settingsRef = useLocalStorage(STORAGE_KEY, DEFAULT_SETTINGS, { - mergeDefaults: true, + const settings = useState(STORAGE_KEY, () => ({ ...DEFAULT_SETTINGS })) + + if (import.meta.client && !syncInitialized) { + syncInitialized = true + + // Read localStorage eagerly but apply after mount to prevent hydration + // mismatch. During hydration, useState provides server-matching defaults. + // After mount, we swap in the user's actual preferences from localStorage. + const stored = readFromLocalStorage() + + onMounted(() => { + settings.value = stored }) + + // Persist future changes back to localStorage + watch( + settings, + value => { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(value)) + } catch {} + }, + { deep: true }, + ) } return { - settings: settingsRef, + settings, } } diff --git a/app/utils/prehydrate.ts b/app/utils/prehydrate.ts index 783d21e3e1..0693b9088e 100644 --- a/app/utils/prehydrate.ts +++ b/app/utils/prehydrate.ts @@ -58,5 +58,10 @@ export function initPreferencesOnPrehydrate() { if (settings.keyboardShortcuts === false) { document.documentElement.dataset.kbdShortcuts = 'false' } + + // Search provider (default: algolia) + if (settings.searchProvider === 'npm') { + document.documentElement.dataset.searchProvider = 'npm' + } }) } From 5da2e862d544946911fe00ee5ed4d0182b2f6de6 Mon Sep 17 00:00:00 2001 From: Adebesin Tolulope Date: Thu, 5 Mar 2026 23:48:25 +0100 Subject: [PATCH 2/2] fix: onMounted fails in plugin context --- app/composables/useSettings.ts | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/app/composables/useSettings.ts b/app/composables/useSettings.ts index 5d02d40ffa..5c9772d073 100644 --- a/app/composables/useSettings.ts +++ b/app/composables/useSettings.ts @@ -74,18 +74,31 @@ const STORAGE_KEY = 'npmx-settings' /** * Read settings from localStorage and merge with defaults. */ +function normaliseSettings(input: AppSettings): AppSettings { + return { + ...input, + searchProvider: input.searchProvider === 'npm' ? 'npm' : 'algolia', + sidebar: { + ...input.sidebar, + collapsed: Array.isArray(input.sidebar?.collapsed) + ? input.sidebar.collapsed.filter((v): v is string => typeof v === 'string') + : [], + }, + } +} + function readFromLocalStorage(): AppSettings { try { const raw = localStorage.getItem(STORAGE_KEY) if (raw) { const stored = JSON.parse(raw) - return { + return normaliseSettings({ ...DEFAULT_SETTINGS, ...stored, connector: { ...DEFAULT_SETTINGS.connector, ...stored.connector }, sidebar: { ...DEFAULT_SETTINGS.sidebar, ...stored.sidebar }, chartFilter: { ...DEFAULT_SETTINGS.chartFilter, ...stored.chartFilter }, - } + }) } } catch {} return { ...DEFAULT_SETTINGS } @@ -111,11 +124,18 @@ export function useSettings() { // Read localStorage eagerly but apply after mount to prevent hydration // mismatch. During hydration, useState provides server-matching defaults. // After mount, we swap in the user's actual preferences from localStorage. + // Uses nuxtApp.hook('app:mounted') instead of onMounted so it works even + // when useSettings() is first called from a plugin (no component context). const stored = readFromLocalStorage() + const nuxtApp = useNuxtApp() - onMounted(() => { + if (nuxtApp.isHydrating) { + nuxtApp.hook('app:mounted', () => { + settings.value = stored + }) + } else { settings.value = stored - }) + } // Persist future changes back to localStorage watch(