the account stores a theme, not a colour mode
Your theme now follows the account instead of living in one browser's localStorage.
Choosing Plain on a laptop reaches the phone.
`appearance.colorMode` is superseded by `appearance.theme`. Along the way this
resolves a mismatch that had been sitting there: the server's default was
`appearance: { theme: 'light' }` while the client read `appearance.colorMode`, so that
default had never once been read by anything. Both sides say `theme` now.
The Light/Dark item is gone from the user menu. Light and dark are two themes, both
already in the list above it, and that toggle was the last writer of the legacy key.
── Two bugs found by testing this, both mine, both silent ──
**A default that looked like a choice.** `useSettings` returns `DEFAULT_SETTINGS`
until the request lands — `settings` is never null, it is a complete valid object. So
"wait for settings" fired on the first render against the defaults, marked itself
done, applied the default theme and never saw the real one. Then the persist side
wrote that default over the account's actual preference. It overwrote the owner's own
`dark` while I was testing it; restored by hand from the `colorMode` it had not yet
destroyed. `useSettings` now exposes `settingsLoaded` so the difference is expressible.
**Defaults masking a migration.** With that fixed, an account holding only
`colorMode: 'dark'` STILL came up light. The client merges `DEFAULT_SETTINGS` over
whatever arrives, so a missing `theme` is filled in with the default before any
consumer sees it — and a default is indistinguishable from a choice. The migration
therefore belongs on the SERVER, which sees the stored object before anything is
merged over it. It is read-only: a GET that rewrites the row on every call is a
surprise, and the client persists on its next change anyway.
Verified by putting the account into the pre-themes shape and asking the new handler
directly, rather than by restarting to find out:
stored: {"colorMode":"dark"} -> returned: {"colorMode":"dark","theme":"duck-dark"}
── Needs a restart ──
The first server-side change in this work. Everything until now was frontend and a
rebuild was enough.
950 pass / 7 fail unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,19 @@ export const useSettings = () => {
|
||||
const { isAuthenticated } = useAuth();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data: settings = DEFAULT_SETTINGS } = useQuery<UserSettings>({
|
||||
const {
|
||||
data: settings = DEFAULT_SETTINGS,
|
||||
/**
|
||||
* Whether the ACCOUNT's settings have arrived, as opposed to the defaults standing in for them.
|
||||
*
|
||||
* `settings` is never null — it falls back to `DEFAULT_SETTINGS`, which is a complete and entirely
|
||||
* valid object. So a consumer that wants to act once on the saved value cannot tell "not loaded yet"
|
||||
* from "loaded, and it happens to equal the defaults". `useColorModeSync` could not, and applied the
|
||||
* default theme on first render, marked itself done, and never saw the real one — then persisted that
|
||||
* default over the account's actual preference.
|
||||
*/
|
||||
isSuccess: settingsLoaded,
|
||||
} = useQuery<UserSettings>({
|
||||
queryKey: QUERY_KEY,
|
||||
enabled: isAuthenticated,
|
||||
queryFn: async () => {
|
||||
@@ -41,7 +53,7 @@ export const useSettings = () => {
|
||||
[client, queryClient],
|
||||
);
|
||||
|
||||
return { settings, saveSettings };
|
||||
return { settings, settingsLoaded, saveSettings };
|
||||
};
|
||||
|
||||
export type UseSettingsType = ReturnType<typeof useSettings>;
|
||||
@@ -66,7 +78,13 @@ export type UserSettings = {
|
||||
defaultModel: string | null;
|
||||
};
|
||||
appearance: {
|
||||
colorMode: 'light' | 'dark';
|
||||
/** The active theme's id — `duck-light`, `duck-dark`, `plain`, … */
|
||||
theme: string;
|
||||
/**
|
||||
* The pre-themes preference, read once to migrate an existing account and never written again.
|
||||
* Optional because a new account has only `theme`.
|
||||
*/
|
||||
colorMode?: 'light' | 'dark';
|
||||
};
|
||||
languages: {
|
||||
spoken: string[];
|
||||
@@ -99,7 +117,7 @@ export const DEFAULT_SETTINGS: UserSettings = {
|
||||
defaultModel: 'claude-code',
|
||||
},
|
||||
appearance: {
|
||||
colorMode: 'light',
|
||||
theme: 'duck-light',
|
||||
},
|
||||
languages: {
|
||||
spoken: ['en'],
|
||||
|
||||
Reference in New Issue
Block a user