Skip to content

Commit c910ddf

Browse files
committed
docs: add Readme for user preferences feature
1 parent bacecd8 commit c910ddf

File tree

2 files changed

+132
-0
lines changed

2 files changed

+132
-0
lines changed

CONTRIBUTING.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ The `.cache/` directory is a separate storage mount used for fetch-cache and atp
166166
app/ # Nuxt 4 app directory
167167
├── components/ # Vue components (PascalCase.vue)
168168
├── composables/ # Vue composables (useFeature.ts)
169+
│ └── userPreferences/ # User preference composables (synced to server)
169170
├── pages/ # File-based routing
170171
├── plugins/ # Nuxt plugins
171172
├── app.vue # Root component
@@ -189,6 +190,9 @@ test/ # Vitest tests
189190
> [!TIP]
190191
> For more about the meaning of these directories, check out the docs on the [Nuxt directory structure](https://nuxt.com/docs/4.x/directory-structure).
191192
193+
> [!TIP]
194+
> For guidance on working with user preferences and local settings, see the [User Preferences README](./app/composables/userPreferences/README.md).
195+
192196
### Local connector CLI
193197

194198
The `cli/` workspace contains a local connector that enables authenticated npm operations from the web UI. It runs on your machine and uses your existing npm credentials.
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# User Preferences
2+
3+
This directory contains composables for managing user preferences — settings that are synced to the server for authenticated users and persisted in `localStorage` for anonymous users.
4+
5+
## Two stores, two purposes
6+
7+
| Store | Composable | localStorage key | Synced to server | Use case |
8+
| -------------------- | --------------------------- | ----------------------- | ------------------- | ------------------------------------------------ |
9+
| **User preferences** | `useUserPreferencesState()` | `npmx-user-preferences` | Yes (authenticated) | Settings the user would expect on another device |
10+
| **Local settings** | `useUserLocalSettings()` | `npmx-settings` | No | Device-specific UI state |
11+
12+
### Decision rule
13+
14+
> Would a user expect this setting to transfer to another browser or device?
15+
>
16+
> - **Yes** → user preference (`npmx-user-preferences`)
17+
> - **No** → local setting (`npmx-settings`)
18+
19+
**User preferences** (synced): accent color, background theme, color mode, locale, search provider, keyboard shortcuts, instant search, relative dates, hide platform packages, include @types in install.
20+
21+
**Local settings** (device-only): chart filter params (average window, smoothing, prediction, anomalies), sidebar collapse state, connector auto-open URL.
22+
23+
## Available composables
24+
25+
| Composable | Purpose |
26+
| ---------------------------------- | ----------------------------------------------------------------- |
27+
| `useUserPreferencesState()` | Read/write access to the full preferences ref |
28+
| `useAccentColor()` | Accent color picker with DOM sync |
29+
| `useBackgroundTheme()` | Background shade with DOM sync |
30+
| `useColorModePreference()` | Color mode synced with `@nuxtjs/color-mode` |
31+
| `useInstantSearchPreference()` | Toggle instant search (shared) |
32+
| `useKeyboardShortcutsPreference()` | Toggle keyboard shortcuts with DOM attribute sync (shared) |
33+
| `useRelativeDatesPreference()` | Read-only computed for relative date display |
34+
| `useSearchProvider()` | npm/algolia toggle |
35+
| `useUserPreferencesSyncStatus()` | Sync status signals (`isSyncing`, `isSynced`, `hasError`) for UI |
36+
| `useInitUserPreferencesSync()` | Imperative `initSync()` — called by the plugin, not by components |
37+
38+
## Adding a new user preference
39+
40+
1. **Add the field to the schema** in `shared/schemas/userPreferences.ts`:
41+
42+
```ts
43+
export const UserPreferencesSchema = object({
44+
// ... existing fields
45+
myNewPref: optional(boolean()),
46+
})
47+
```
48+
49+
2. **Add a default value** in `DEFAULT_USER_PREFERENCES` (same file):
50+
51+
```ts
52+
export const DEFAULT_USER_PREFERENCES = {
53+
// ... existing defaults
54+
myNewPref: false,
55+
}
56+
```
57+
58+
3. **Create a composable** in this directory (e.g. `useMyNewPref.ts`):
59+
60+
```ts
61+
export function useMyNewPref() {
62+
const { preferences } = useUserPreferencesState()
63+
64+
return computed({
65+
get: () => preferences.value.myNewPref ?? false,
66+
set: (value: boolean) => {
67+
preferences.value.myNewPref = value
68+
},
69+
})
70+
}
71+
```
72+
73+
4. **Use it in components** — the composable is auto-imported:
74+
75+
```vue
76+
<script setup lang="ts">
77+
const myNewPref = useMyNewPref()
78+
</script>
79+
```
80+
81+
The preference will automatically persist to localStorage and sync to the server for authenticated users. No additional wiring needed.
82+
83+
## Adding a new local setting
84+
85+
1. **Add the field** to the `UserLocalSettings` interface and `DEFAULT_USER_LOCAL_SETTINGS` in `app/composables/useUserLocalSettings.ts`:
86+
87+
```ts
88+
export interface UserLocalSettings {
89+
// ... existing fields
90+
myLocalThing: boolean
91+
}
92+
93+
const DEFAULT_USER_LOCAL_SETTINGS: UserLocalSettings = {
94+
// ... existing defaults
95+
myLocalThing: false,
96+
}
97+
```
98+
99+
2. **Use it in components:**
100+
101+
```vue
102+
<script setup lang="ts">
103+
const { localSettings } = useUserLocalSettings()
104+
// localSettings.value.myLocalThing
105+
</script>
106+
```
107+
108+
## Architecture overview
109+
110+
```
111+
useUserPreferencesProvider ← singleton, manages localStorage + sync lifecycle
112+
├── useUserPreferencesSync ← client-only: debounced server writes, route guard, sendBeacon
113+
├── useUserPreferencesState ← read/write access to reactive ref (used by all composables above)
114+
└── preferences-merge.ts ← merge logic for first-login vs returning-user scenarios
115+
116+
useUserLocalSettings ← separate singleton, localStorage only, no sync
117+
118+
useLocalStorageHashProvider ← generic localStorage + defu provider (used by usePackageListPreferences)
119+
```
120+
121+
### Sync flow (authenticated users)
122+
123+
1. `preferences-sync.client.ts` plugin calls `initSync()` on app boot
124+
2. Preferences are loaded from server and merged with local state
125+
3. A deep watcher on the preferences ref triggers `scheduleSync()` on every change
126+
4. `scheduleSync()` debounces for 2 seconds, then pushes to `PUT /api/user/preferences`
127+
5. On route navigation, `router.beforeEach` flushes any pending sync
128+
6. On tab close, `sendBeacon` fires the latest preferences via `POST /api/user/preferences`

0 commit comments

Comments
 (0)