diff --git a/frontend/src/lib/api/endpoints/profile.ts b/frontend/src/lib/api/endpoints/profile.ts index b5e21cdb..0360168b 100644 --- a/frontend/src/lib/api/endpoints/profile.ts +++ b/frontend/src/lib/api/endpoints/profile.ts @@ -12,6 +12,16 @@ export interface ProfilePatch { family_name?: string; preferred_locale?: string; notify_on_share?: boolean; + /** + * Partial patch into the opaque UI preferences bag. Server does a + * SHALLOW merge — keys present here overwrite existing top-level + * keys; absent keys survive. Set a key to `null` to remove it + * (server runs `jsonb_strip_nulls` after the merge). + * + * Wire-side type is `Record`; the typed view over + * this bag lives in `lib/stores/preferences.svelte.ts`. + */ + ui_preferences?: Record; } export async function updateProfile(patch: ProfilePatch): Promise { diff --git a/frontend/src/lib/api/types.ts b/frontend/src/lib/api/types.ts index 82d695f1..69a3db6a 100644 --- a/frontend/src/lib/api/types.ts +++ b/frontend/src/lib/api/types.ts @@ -172,6 +172,22 @@ export interface User { email_verified_at?: string; preferred_locale?: string; notify_on_share: boolean; + /** + * Opaque UI preferences bag. Server-side JSONB column that persists + * pure UI toggles (hide-dotfiles, view mode, sidebar collapse, …) + * across devices. The server never inspects the contents — the SPA + * defines the keys (see `lib/stores/preferences.svelte.ts` for the + * typed view). Always an object on the wire (empty bag is `{}`, + * never `null` or missing). + * + * When PATCHing back to the server via + * `PATCH /api/auth/me/profile { ui_preferences: {...} }`, the + * server SHALLOW-merges — only the keys present in the patch are + * touched, so partial writes from one device don't clobber + * preferences set on another. Set a key to `null` in the patch to + * delete it from the bag. + */ + ui_preferences: Record; } export interface AuthResponse { diff --git a/frontend/src/lib/components/AppShell.svelte b/frontend/src/lib/components/AppShell.svelte index 57577510..09318d76 100644 --- a/frontend/src/lib/components/AppShell.svelte +++ b/frontend/src/lib/components/AppShell.svelte @@ -14,6 +14,7 @@ import { userInitials, avatarColorIndex } from '$lib/utils/avatar'; import { i18n, LANGUAGES, setLocale, t, type Locale } from '$lib/i18n/index.svelte'; import { apiFetch } from '$lib/api/client'; + import { preferences } from '$lib/stores/preferences.svelte'; import { session } from '$lib/stores/session.svelte'; import { theme, type Theme } from '$lib/stores/theme.svelte'; import { ui } from '$lib/stores/ui.svelte'; @@ -208,6 +209,19 @@ langOpen = false; } + /** + * True when the shortcut target is a text-input surface — , + *