Skip to content

Commit 96ad934

Browse files
committed
feat: replace useSettings with useUserPreferences
- extract sidebar collapsed state into separate `usePackageSidebarPreferences` composable - add `preferences-sync.client.ts` plugin for early color mode + server sync init - wrap theme select in `<ClientOnly>` to prevent SSR hydration mismatch - show sync status indicator on settings page for authenticated users - add `useColorModePreference` composable to sync color mode with `@nuxtjs/color-mode` with '#' will be ignored, and an empty message aborts the commit. # # On branch feat-484-persist-user-preferences # Changes to be committed: app/components/Settings/AccentColorPicker.vue # modified: app/components/Settings/BgThemePicker.vue # modified: app/composables/useInstallCommand.ts # new file: app/composables/usePackageSidebarPreferences.ts # deleted: app/composables/useSettings.ts # new file: app/composables/useUserPreferences.ts # new file: app/composables/useUserPreferencesProvider.ts # modified: app/pages/search.vue # modified: app/pages/settings.vue # modified: app/plugins/i18n-loader.client.ts # new file: app/plugins/preferences-sync.client.ts # modified: app/utils/prehydrate.ts # modified: i18n/locales/en.json # modified: lunaria/files/en-US.json # modified: shared/schemas/userPreferences.ts # modified: test/nuxt/components/DateTime.spec.ts # modified: test/nuxt/components/compare/FacetRow.spec.ts # modified: test/nuxt/composables/use-install-command.spec.ts # # Untracked files:
1 parent 5b380e4 commit 96ad934

File tree

20 files changed

+385
-217
lines changed

20 files changed

+385
-217
lines changed

app/components/CollapsibleSection.vue

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const props = withDefaults(defineProps<Props>(), {
1414
headingLevel: 'h2',
1515
})
1616
17-
const appSettings = useSettings()
17+
const { sidebarPreferences } = usePackageSidebarPreferences()
1818
1919
const buttonId = `${props.id}-collapsible-button`
2020
const contentId = `${props.id}-collapsible-content`
@@ -23,8 +23,8 @@ const headingId = `${props.id}-heading`
2323
const isOpen = shallowRef(true)
2424
2525
onPrehydrate(() => {
26-
const settings = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
27-
const collapsed: string[] = settings?.sidebar?.collapsed || []
26+
const sidebar = JSON.parse(localStorage.getItem('npmx-sidebar-preferences') || '{}')
27+
const collapsed: string[] = sidebar?.collapsed || []
2828
for (const id of collapsed) {
2929
if (!document.documentElement.dataset.collapsed?.includes(id)) {
3030
document.documentElement.dataset.collapsed = (
@@ -45,17 +45,16 @@ onMounted(() => {
4545
function toggle() {
4646
isOpen.value = !isOpen.value
4747
48-
const removed = appSettings.settings.value.sidebar.collapsed.filter(c => c !== props.id)
48+
const removed = sidebarPreferences.value.collapsed.filter(c => c !== props.id)
4949
5050
if (isOpen.value) {
51-
appSettings.settings.value.sidebar.collapsed = removed
51+
sidebarPreferences.value.collapsed = removed
5252
} else {
5353
removed.push(props.id)
54-
appSettings.settings.value.sidebar.collapsed = removed
54+
sidebarPreferences.value.collapsed = removed
5555
}
5656
57-
document.documentElement.dataset.collapsed =
58-
appSettings.settings.value.sidebar.collapsed.join(' ')
57+
document.documentElement.dataset.collapsed = sidebarPreferences.value.collapsed.join(' ')
5958
}
6059
6160
const ariaLabel = computed(() => {

app/components/Settings/AccentColorPicker.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<script setup lang="ts">
2-
import { useAccentColor } from '~/composables/useSettings'
2+
import { useAccentColor } from '~/composables/useUserPreferences'
33
44
const { accentColors, selectedAccentColor, setAccentColor } = useAccentColor()
55
66
onPrehydrate(el => {
7-
const settings = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
8-
const id = settings.accentColorId
7+
const preferences = JSON.parse(localStorage.getItem('npmx-user-preferences') || '{}')
8+
const id = preferences.accentColorId
99
if (id) {
1010
const input = el.querySelector<HTMLInputElement>(`input[value="${id}"]`)
1111
if (input) {

app/components/Settings/BgThemePicker.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
const { backgroundThemes, selectedBackgroundTheme, setBackgroundTheme } = useBackgroundTheme()
33
44
onPrehydrate(el => {
5-
const settings = JSON.parse(localStorage.getItem('npmx-settings') || '{}')
6-
const id = settings.preferredBackgroundTheme
5+
const preferences = JSON.parse(localStorage.getItem('npmx-user-preferences') || '{}')
6+
const id = preferences.preferredBackgroundTheme
77
if (id) {
88
const input = el.querySelector<HTMLInputElement>(`input[value="${id}"]`)
99
if (input) {

app/composables/useInstallCommand.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ export function useInstallCommand(
1111
typesPackageName: MaybeRefOrGetter<string | null>,
1212
) {
1313
const selectedPM = useSelectedPackageManager()
14-
const { settings } = useSettings()
14+
const { preferences } = useUserPreferences()
1515

1616
// Check if we should show @types in install command
1717
const showTypesInInstall = computed(() => {
18-
return settings.value.includeTypesInInstall && !!toValue(typesPackageName)
18+
return preferences.value.includeTypesInInstall && !!toValue(typesPackageName)
1919
})
2020

2121
const installCommandParts = computed(() => {
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
interface SidebarPreferences {
2+
collapsed: string[]
3+
}
4+
5+
const STORAGE_KEY = 'npmx-sidebar-preferences'
6+
const DEFAULT_SIDEBAR_PREFERENCES: SidebarPreferences = { collapsed: [] }
7+
8+
let sidebarRef: Ref<SidebarPreferences> | null = null
9+
10+
/**
11+
* Composable for managing sidebar section collapsed state.
12+
* This is local-only and uses its own LS key.
13+
*/
14+
export function usePackageSidebarPreferences() {
15+
if (!sidebarRef) {
16+
sidebarRef = useLocalStorage<SidebarPreferences>(STORAGE_KEY, DEFAULT_SIDEBAR_PREFERENCES, {
17+
mergeDefaults: true,
18+
})
19+
}
20+
21+
return {
22+
sidebarPreferences: sidebarRef,
23+
}
24+
}

app/composables/useSettings.ts

Lines changed: 0 additions & 150 deletions
This file was deleted.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { ACCENT_COLORS, BACKGROUND_THEMES } from '#shared/utils/constants'
2+
import {
3+
DEFAULT_USER_PREFERENCES,
4+
type AccentColorId,
5+
type BackgroundThemeId,
6+
type ColorModePreference,
7+
type UserPreferences,
8+
} from '#shared/schemas/userPreferences'
9+
10+
/**
11+
* Main composable for user preferences.
12+
* Uses `npmx-user-preferences` localStorage key and syncs to the server
13+
* for authenticated users via `useUserPreferencesProvider`.
14+
*/
15+
export function useUserPreferences() {
16+
const provider = useUserPreferencesProvider<UserPreferences>(DEFAULT_USER_PREFERENCES)
17+
18+
return {
19+
preferences: provider.data,
20+
isAuthenticated: provider.isAuthenticated,
21+
isSyncing: provider.isSyncing,
22+
isSynced: provider.isSynced,
23+
hasError: provider.hasError,
24+
syncError: provider.syncError,
25+
lastSyncedAt: provider.lastSyncedAt,
26+
initSync: provider.initSync,
27+
}
28+
}
29+
30+
export function useRelativeDates() {
31+
const { preferences } = useUserPreferences()
32+
return computed(() => preferences.value.relativeDates)
33+
}
34+
35+
export function useAccentColor() {
36+
const { preferences } = useUserPreferences()
37+
const colorMode = useColorMode()
38+
39+
const accentColors = computed(() => {
40+
const isDark = colorMode.value === 'dark'
41+
const colors = isDark ? ACCENT_COLORS.dark : ACCENT_COLORS.light
42+
43+
return Object.entries(colors).map(([id, value]) => ({
44+
id: id as AccentColorId,
45+
name: id,
46+
value,
47+
}))
48+
})
49+
50+
function setAccentColor(id: AccentColorId | null) {
51+
if (id) {
52+
document.documentElement.style.setProperty('--accent-color', `var(--swatch-${id})`)
53+
} else {
54+
document.documentElement.style.removeProperty('--accent-color')
55+
}
56+
preferences.value.accentColorId = id
57+
}
58+
59+
return {
60+
accentColors,
61+
selectedAccentColor: computed(() => preferences.value.accentColorId),
62+
setAccentColor,
63+
}
64+
}
65+
66+
export function useBackgroundTheme() {
67+
const backgroundThemes = Object.entries(BACKGROUND_THEMES).map(([id, value]) => ({
68+
id: id as BackgroundThemeId,
69+
name: id,
70+
value,
71+
}))
72+
73+
const { preferences } = useUserPreferences()
74+
75+
function setBackgroundTheme(id: BackgroundThemeId | null) {
76+
if (id) {
77+
document.documentElement.dataset.bgTheme = id
78+
} else {
79+
document.documentElement.removeAttribute('data-bg-theme')
80+
}
81+
preferences.value.preferredBackgroundTheme = id
82+
}
83+
84+
return {
85+
backgroundThemes,
86+
selectedBackgroundTheme: computed(() => preferences.value.preferredBackgroundTheme),
87+
setBackgroundTheme,
88+
}
89+
}
90+
91+
/**
92+
* Composable for syncing color mode preference.
93+
* Keeps the user preference in sync with @nuxtjs/color-mode's own LS key (`npmx-color-mode`)
94+
* so that the color-mode module picks up the correct value on page load.
95+
*/
96+
export function useColorModePreference() {
97+
const { preferences } = useUserPreferences()
98+
const colorMode = useColorMode()
99+
100+
/**
101+
* Set color mode preference and sync to both user preferences and the
102+
* `npmx-color-mode` LS key used by @nuxtjs/color-mode.
103+
*/
104+
function setColorMode(mode: ColorModePreference) {
105+
preferences.value.colorModePreference = mode
106+
colorMode.preference = mode
107+
}
108+
109+
/**
110+
* On init, if the user has a stored preference, apply it to @nuxtjs/color-mode.
111+
* This handles the case where preferences were synced from the server.
112+
*/
113+
function applyStoredColorMode() {
114+
const stored = preferences.value.colorModePreference
115+
if (stored) {
116+
colorMode.preference = stored
117+
} else {
118+
// No user preference stored yet — seed it from the current color-mode LS value
119+
const currentPreference = colorMode.preference as ColorModePreference
120+
if (currentPreference && currentPreference !== 'system') {
121+
preferences.value.colorModePreference = currentPreference
122+
}
123+
}
124+
}
125+
126+
return {
127+
colorModePreference: computed(
128+
() => preferences.value.colorModePreference ?? colorMode.preference,
129+
),
130+
setColorMode,
131+
applyStoredColorMode,
132+
}
133+
}

0 commit comments

Comments
 (0)