From 555cb76a4970686f52d51878de7524b4b2ef2902 Mon Sep 17 00:00:00 2001 From: jadonamite Date: Wed, 27 May 2026 19:47:02 +0100 Subject: [PATCH] docs: add TSDoc documentation to User Settings module Add comprehensive TSDoc comments to the User Settings implementation: src/lib/settings/types.ts: - Document each AppSettings field (theme, language, notifications, prefetching, reducedMotion) with purpose and valid values - Describe the export envelope schema and its migration intent - Document createDefaultSettings and its rationale for each default src/lib/settings/store.ts: - Add module-level JSDoc with quick-start usage example, persistence explanation, sync strategy, and export/import pointers - Document each store action (patchSettings, replaceSettings, resetSettings, setLastSyncedAt) with behavior, validation notes, and side-effects - Document store state fields (settings, updatedAt, lastSyncedAt) Closes #533 --- src/lib/settings/store.ts | 64 +++++++++++++++++++++++++++++++++++++++ src/lib/settings/types.ts | 32 ++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/src/lib/settings/store.ts b/src/lib/settings/store.ts index d3740569..0a931e50 100644 --- a/src/lib/settings/store.ts +++ b/src/lib/settings/store.ts @@ -1,20 +1,84 @@ 'use client'; +/** + * @module settings/store + * + * Zustand store for user application settings. + * + * ## Quick start + * + * ```tsx + * import { useSettingsStore } from '@/lib/settings/store'; + * + * function MyComponent() { + * const theme = useSettingsStore((s) => s.settings.theme); + * const patchSettings = useSettingsStore((s) => s.patchSettings); + * + * return ( + * + * ); + * } + * ``` + * + * ## Persistence + * + * Settings are automatically persisted to `localStorage` under the key defined by + * `SETTINGS_STORAGE_KEY`. The store handles SSR gracefully by falling back to a no-op + * storage when `window` is unavailable. + * + * ## Sync + * + * Use `fetchRemoteSettings` / `pushRemoteSettings` from `@/lib/settings/sync` to + * synchronise settings with the server. `updatedAt` acts as a vector clock: the + * side with the larger value wins. + * + * ## Export / Import + * + * Use `buildExportEnvelope` / `parseExportedSettings` from `@/lib/settings/export-import` + * to serialise settings to a portable JSON file that users can download and re-import. + */ import { create } from 'zustand'; import { persist, createJSONStorage } from 'zustand/middleware'; import { SETTINGS_SCHEMA_VERSION, SETTINGS_STORAGE_KEY } from './constants'; import { type AppSettings, appSettingsSchema, createDefaultSettings } from './types'; interface SettingsStoreActions { + /** + * Merge a partial update into the current settings. + * Validates the merged result against `appSettingsSchema`; silently ignores invalid patches. + * `language` is trimmed and clamped to 24 chars; empty strings fall back to `'en'`. + * Automatically updates `updatedAt` to `Date.now()`. + */ patchSettings: (partial: Partial) => void; + + /** + * Overwrite all settings with a validated `AppSettings` object (e.g. after a remote sync or import). + * Pass `markSynced = true` to set `lastSyncedAt` to `updatedAt` in the same write. + * Invalid settings objects are silently dropped. + */ replaceSettings: (settings: AppSettings, updatedAt: number, markSynced?: boolean) => void; + + /** + * Restore all settings to the application defaults (see `createDefaultSettings`). + * Resets `updatedAt` to `Date.now()` and clears `lastSyncedAt`. + */ resetSettings: () => void; + + /** + * Record the timestamp of the last successful remote sync. + * Pass `null` to clear the sync marker (e.g. after a reset). + */ setLastSyncedAt: (t: number | null) => void; } interface SettingsSlice extends SettingsStoreActions { + /** Current user preferences. Always conforms to `appSettingsSchema`. */ settings: AppSettings; + /** Unix ms timestamp of the last local settings mutation. Used as a vector clock for sync. */ updatedAt: number; + /** Unix ms timestamp of the most recent successful remote sync, or `null` if never synced. */ lastSyncedAt: number | null; } diff --git a/src/lib/settings/types.ts b/src/lib/settings/types.ts index 28e6c7d9..8c724dbf 100644 --- a/src/lib/settings/types.ts +++ b/src/lib/settings/types.ts @@ -1,9 +1,22 @@ import { z } from 'zod'; import { SETTINGS_SCHEMA_VERSION } from './constants'; +/** User-selectable colour scheme. `'system'` follows the OS preference. */ export const themePreferenceSchema = z.enum(['light', 'dark', 'system']); export type ThemePreference = z.infer; +/** + * Validated schema for all user-configurable application settings. + * + * Fields: + * - `version` — Schema version; bumped when new fields are added (see `SETTINGS_SCHEMA_VERSION`). + * - `theme` — Colour scheme: `'light'`, `'dark'`, or `'system'` (follows OS preference). + * - `language` — BCP-47 locale tag (e.g. `'en'`, `'fr-CA'`), max 24 chars; defaults to `navigator.language`. + * - `notificationsEnabled` — Master toggle for in-app push/toast notifications. + * - `emailNotifications` — Whether transactional and digest emails should be sent. + * - `prefetchingEnabled` — Pre-fetches linked pages on hover for faster navigation; disable on slow connections. + * - `reducedMotion` — Suppresses non-essential animations for users who prefer reduced motion. + */ export const appSettingsSchema = z.object({ version: z.literal(SETTINGS_SCHEMA_VERSION), theme: themePreferenceSchema, @@ -14,18 +27,30 @@ export const appSettingsSchema = z.object({ reducedMotion: z.boolean(), }); +/** Fully typed representation of all user settings. Inferred from `appSettingsSchema`. */ export type AppSettings = z.infer; +/** + * Shape persisted by the Zustand store to `localStorage` (key: `SETTINGS_STORAGE_KEY`). + * Includes `updatedAt` and `lastSyncedAt` for conflict resolution during remote sync. + */ export const settingsStoreStateSchema = z.object({ settings: appSettingsSchema, + /** Unix ms timestamp of the last local mutation. Used as a vector clock for sync. */ updatedAt: z.number(), + /** Unix ms timestamp of the last successful remote sync, or `null` if never synced. */ lastSyncedAt: z.number().nullable(), }); export type SettingsStorePersistedShape = z.infer; +/** + * JSON envelope produced by `buildExportEnvelope` and consumed by `parseExportedSettings`. + * Including `exportedAt` and `version` allows future migrations to detect stale exports. + */ export const exportedSettingsEnvelopeSchema = z.object({ version: z.literal(SETTINGS_SCHEMA_VERSION), + /** ISO-8601 UTC timestamp of when the file was exported. */ exportedAt: z.string(), settings: appSettingsSchema, updatedAt: z.number(), @@ -33,6 +58,13 @@ export const exportedSettingsEnvelopeSchema = z.object({ export type ExportedSettingsEnvelope = z.infer; +/** + * Returns an `AppSettings` object with safe, well-defined defaults. + * + * - `theme` defaults to `'system'` so the OS preference is respected out of the box. + * - `language` is read from `navigator.language` when available, falling back to `'en'`. + * - All notification and UX toggles default to their most permissive value. + */ export function createDefaultSettings(): AppSettings { return { version: SETTINGS_SCHEMA_VERSION,