An explicit theme choice (even Midnight Sky) wins over the sky #114
7 changed files with 75 additions and 16 deletions
|
|
@ -146,8 +146,12 @@ describe('sceneInk: an explicit theme wins over the auto ink', () => {
|
|||
});
|
||||
// auto, but the person chose a theme: it wins, the theme's own ink stands.
|
||||
expect(sceneInk(sceneThemeId('auto', 'dusk', true, true), true, 'dusk', true)).toBeUndefined();
|
||||
// a fixed sky state is itself a choice and wins over the theme (skyTheme.ts), so its ink applies.
|
||||
expect(sceneInk(sceneThemeId('night', 'night', true, true), true, 'night', false)).toEqual({
|
||||
// a fixed sky state does not override the person's theme either: the theme's own ink stands.
|
||||
expect(
|
||||
sceneInk(sceneThemeId('night', 'night', true, true), true, 'night', false)
|
||||
).toBeUndefined();
|
||||
// a fixed sky state with no theme of the person's own: the sky theme's ink applies.
|
||||
expect(sceneInk(sceneThemeId('night', 'night', true, false), true, 'night', false)).toEqual({
|
||||
state: 'night',
|
||||
phone: false,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -53,12 +53,18 @@ describe('skyThemeId', () => {
|
|||
});
|
||||
|
||||
describe('sceneThemeId, the theme the page wears', () => {
|
||||
it('a fixed state always picks its theme, whatever the scene or the person chose', () => {
|
||||
[true, false].forEach((scene) =>
|
||||
[true, false].forEach((chose) => {
|
||||
expect(sceneThemeId('day', 'night', scene, chose)).toBe('noonday-sky');
|
||||
expect(sceneThemeId('night', 'day', scene, chose)).toBe('midnight-sky');
|
||||
})
|
||||
it('a fixed state picks its theme, whatever the scene, while the person chose no theme', () => {
|
||||
[true, false].forEach((scene) => {
|
||||
expect(sceneThemeId('day', 'night', scene, false)).toBe('noonday-sky');
|
||||
expect(sceneThemeId('night', 'day', scene, false)).toBe('midnight-sky');
|
||||
});
|
||||
});
|
||||
|
||||
it('a theme the person chose wins over every sky state, fixed or auto', () => {
|
||||
(['auto', 'day', 'dusk', 'night'] as const).forEach((state) =>
|
||||
[true, false].forEach((scene) =>
|
||||
expect(sceneThemeId(state, 'night', scene, true)).toBeUndefined()
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ export const skyThemeId = (state: SkyState): SkyThemeId | undefined =>
|
|||
state === 'auto' ? undefined : BY_STATE[state];
|
||||
|
||||
/**
|
||||
* The theme the page wears: a fixed sky state always picks its theme; 'auto' follows the resolved sky (`resolved`, the
|
||||
* clock's state) only while a scene is on and the person has not chosen a theme of their own, and otherwise picks none.
|
||||
* The theme the page wears: a theme the person chose always wins, and picks none here. Otherwise a fixed sky state
|
||||
* picks its theme, and 'auto' follows the resolved sky (`resolved`, the clock's state) only while a scene is on.
|
||||
* Signed-out pages choose no theme, so they pass `userChoseTheme: false`.
|
||||
*/
|
||||
export const sceneThemeId = (
|
||||
|
|
@ -34,6 +34,7 @@ export const sceneThemeId = (
|
|||
sceneOn: boolean,
|
||||
userChoseTheme: boolean
|
||||
): SkyThemeId | undefined => {
|
||||
if (userChoseTheme) return undefined;
|
||||
if (state !== 'auto') return BY_STATE[state];
|
||||
return sceneOn && !userChoseTheme ? BY_STATE[resolved] : undefined;
|
||||
return sceneOn ? BY_STATE[resolved] : undefined;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ function SelectTheme({ disabled }: { disabled?: boolean }) {
|
|||
const themes = useThemes();
|
||||
const themeNames = useThemeNames();
|
||||
const [themeId, setThemeId] = useSetting(settingsAtom, 'themeId');
|
||||
const [, setThemeChosen] = useSetting(settingsAtom, 'themeChosen');
|
||||
const [menuCords, setMenuCords] = useState<RectCords>();
|
||||
const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme;
|
||||
|
||||
|
|
@ -90,6 +91,7 @@ function SelectTheme({ disabled }: { disabled?: boolean }) {
|
|||
|
||||
const handleThemeSelect = (theme: Theme) => {
|
||||
setThemeId(theme.id);
|
||||
setThemeChosen(true);
|
||||
setMenuCords(undefined);
|
||||
};
|
||||
|
||||
|
|
|
|||
40
src/app/hooks/useTheme.test.ts
Normal file
40
src/app/hooks/useTheme.test.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { readFileSync } from 'fs';
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
// settings.ts reads localStorage when it loads, and this suite runs without a DOM.
|
||||
vi.stubGlobal('localStorage', { getItem: () => null, setItem: () => undefined });
|
||||
let chosenTheme: typeof import('./useTheme').chosenTheme;
|
||||
let defaultThemeId: string;
|
||||
beforeAll(async () => {
|
||||
({ chosenTheme } = await import('./useTheme'));
|
||||
({ defaultThemeId } = await import('../state/settings'));
|
||||
});
|
||||
|
||||
describe('chosenTheme: what counts as a theme the person chose', () => {
|
||||
it('is not a theme before anything was picked: no id saved, or the default saved', () => {
|
||||
expect(chosenTheme(false, undefined, undefined)).toBe(false);
|
||||
expect(chosenTheme(false, defaultThemeId, undefined)).toBe(false);
|
||||
expect(chosenTheme(false, defaultThemeId, false)).toBe(false);
|
||||
});
|
||||
|
||||
it('is a pick of Midnight Sky made in the theme menu, which sets the flag', () => {
|
||||
expect(chosenTheme(false, defaultThemeId, true)).toBe(true);
|
||||
});
|
||||
|
||||
it('is a saved id other than the default, with or without the flag', () => {
|
||||
expect(chosenTheme(false, 'dark-theme', undefined)).toBe(true);
|
||||
expect(chosenTheme(false, 'noonday-sky', true)).toBe(true);
|
||||
});
|
||||
|
||||
it('is the system scheme', () => {
|
||||
expect(chosenTheme(true, defaultThemeId, undefined)).toBe(true);
|
||||
});
|
||||
|
||||
it('the theme menu sets the flag together with the id', () => {
|
||||
const tsx = readFileSync(
|
||||
`${process.cwd()}/src/app/features/settings/general/General.tsx`,
|
||||
'utf8'
|
||||
);
|
||||
expect(tsx).toMatch(/setThemeId\(theme\.id\);\s*setThemeChosen\(true\);/);
|
||||
});
|
||||
});
|
||||
|
|
@ -107,9 +107,12 @@ export const useSystemThemeKind = (): ThemeKind => {
|
|||
const toTheme = (id: ReturnType<typeof sceneThemeId>): Theme | undefined =>
|
||||
id === NoondaySkyTheme.id ? NoondaySkyTheme : id && MidnightSkyTheme;
|
||||
|
||||
/** A theme the person chose: the system scheme, or one other than the default. A saved Midnight Sky is the default. */
|
||||
const chosenTheme = (useSystemTheme: boolean, themeId?: string) =>
|
||||
useSystemTheme || (themeId !== undefined && themeId !== defaultThemeId);
|
||||
/**
|
||||
* A theme the person chose: the system scheme, any pick made in the theme menu (`themeChosen`, which also covers
|
||||
* Midnight Sky, the default), or a saved id other than the default (picks from before the flag existed).
|
||||
*/
|
||||
export const chosenTheme = (useSystemTheme: boolean, themeId?: string, themeChosen?: boolean) =>
|
||||
useSystemTheme || themeChosen === true || (themeId !== undefined && themeId !== defaultThemeId);
|
||||
|
||||
/** The theme the sky asks for on a page with no theme of its own (signed out), or undefined for none. */
|
||||
export const useSkyTheme = (): Theme | undefined =>
|
||||
|
|
@ -122,8 +125,9 @@ const useSceneSkyThemeId = (): ReturnType<typeof sceneThemeId> => {
|
|||
const sceneOn = useSceneOn();
|
||||
const [systemTheme] = useSetting(settingsAtom, 'useSystemTheme');
|
||||
const [themeId] = useSetting(settingsAtom, 'themeId');
|
||||
// A person's own theme wins over 'auto'; a fixed sky state is itself a choice and wins over the theme.
|
||||
return sceneThemeId(sky, resolved, sceneOn, chosenTheme(systemTheme, themeId));
|
||||
const [themeChosen] = useSetting(settingsAtom, 'themeChosen');
|
||||
// A person's own theme wins over the sky, fixed or 'auto'.
|
||||
return sceneThemeId(sky, resolved, sceneOn, chosenTheme(systemTheme, themeId, themeChosen));
|
||||
};
|
||||
|
||||
/** The ink the page wears over the scene (`sceneInk`, skyInk.ts), or undefined when the theme's own ink stands. */
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ export enum MessageLayout {
|
|||
|
||||
export interface Settings {
|
||||
themeId?: string;
|
||||
/** Set when a theme is picked in the menu, so a picked default (Midnight Sky) reads as chosen. Absent until then. */
|
||||
themeChosen?: boolean;
|
||||
useSystemTheme: boolean;
|
||||
lightThemeId?: string;
|
||||
darkThemeId?: string;
|
||||
|
|
|
|||
Loading…
Reference in a new issue