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,