|
1 | | -import type { RemovableRef } from '@vueuse/core' |
2 | | -import { useLocalStorage } from '@vueuse/core' |
3 | 1 | import { ACCENT_COLORS } from '#shared/utils/constants' |
4 | 2 | import type { LocaleObject } from '@nuxtjs/i18n' |
5 | 3 | import { BACKGROUND_THEMES } from '#shared/utils/constants' |
@@ -72,22 +70,66 @@ const DEFAULT_SETTINGS: AppSettings = { |
72 | 70 |
|
73 | 71 | const STORAGE_KEY = 'npmx-settings' |
74 | 72 |
|
75 | | -// Shared settings instance (singleton per app) |
76 | | -let settingsRef: RemovableRef<AppSettings> | null = null |
| 73 | +/** |
| 74 | + * Read settings from localStorage and merge with defaults. |
| 75 | + */ |
| 76 | +function readFromLocalStorage(): AppSettings { |
| 77 | + try { |
| 78 | + const raw = localStorage.getItem(STORAGE_KEY) |
| 79 | + if (raw) { |
| 80 | + const stored = JSON.parse(raw) |
| 81 | + return { |
| 82 | + ...DEFAULT_SETTINGS, |
| 83 | + ...stored, |
| 84 | + connector: { ...DEFAULT_SETTINGS.connector, ...stored.connector }, |
| 85 | + sidebar: { ...DEFAULT_SETTINGS.sidebar, ...stored.sidebar }, |
| 86 | + chartFilter: { ...DEFAULT_SETTINGS.chartFilter, ...stored.chartFilter }, |
| 87 | + } |
| 88 | + } |
| 89 | + } catch {} |
| 90 | + return { ...DEFAULT_SETTINGS } |
| 91 | +} |
| 92 | + |
| 93 | +let syncInitialized = false |
77 | 94 |
|
78 | 95 | /** |
79 | | - * Composable for managing application settings with localStorage persistence. |
80 | | - * Settings are shared across all components that use this composable. |
| 96 | + * Composable for managing application settings. |
| 97 | + * |
| 98 | + * Uses useState for SSR-safe hydration (server and client agree on initial |
| 99 | + * values during hydration) and syncs with localStorage on the client. |
| 100 | + * The onPrehydrate script in prehydrate.ts handles DOM-level patches |
| 101 | + * (accent color, bg theme, collapsed sections, etc.) to prevent visual |
| 102 | + * flash before hydration. |
81 | 103 | */ |
82 | 104 | export function useSettings() { |
83 | | - if (!settingsRef) { |
84 | | - settingsRef = useLocalStorage<AppSettings>(STORAGE_KEY, DEFAULT_SETTINGS, { |
85 | | - mergeDefaults: true, |
| 105 | + const settings = useState<AppSettings>(STORAGE_KEY, () => ({ ...DEFAULT_SETTINGS })) |
| 106 | + |
| 107 | + if (import.meta.client && !syncInitialized) { |
| 108 | + syncInitialized = true |
| 109 | + |
| 110 | + // Read localStorage eagerly but apply after mount to prevent hydration |
| 111 | + // mismatch. During hydration, useState provides server-matching defaults. |
| 112 | + // After mount, we swap in the user's actual preferences from localStorage. |
| 113 | + const stored = readFromLocalStorage() |
| 114 | + |
| 115 | + onMounted(() => { |
| 116 | + settings.value = stored |
86 | 117 | }) |
| 118 | + |
| 119 | + // Persist future changes back to localStorage |
| 120 | + watch( |
| 121 | + settings, |
| 122 | + value => { |
| 123 | + try { |
| 124 | + localStorage.setItem(STORAGE_KEY, JSON.stringify(value)) |
| 125 | + } catch {} |
| 126 | + }, |
| 127 | + { deep: true }, |
| 128 | + ) |
87 | 129 | } |
88 | 130 |
|
89 | 131 | return { |
90 | | - settings: settingsRef, |
| 132 | + settings, |
91 | 133 | } |
92 | 134 | } |
93 | 135 |
|
|
0 commit comments