feat(ui): Orca Dark / Orca Light themes and notification sounds

Two optional named themes from Orca's neutral palette join the Appearance
picker (Orca Dark keeps a blue accent so white-on-accent buttons stay
legible; Orca Light's status colours are darkened to clear 4.5:1 on their
chips). A shown notification can play a sound — None (default), Chime, Pop
or Alert — chosen globally and per notification type in Settings →
Notifications; the WAVs are generated by scripts/gen-notification-sounds.js.
This commit is contained in:
2026-10-01 17:10:46 +07:00
parent eb90244182
commit b71024f244
20 changed files with 487 additions and 55 deletions
@@ -0,0 +1,45 @@
/**
* @file notifPrefs.test.ts
* @description Notification prefs: the defaults (sound "none"), a blob stored
* before sounds existed loading with sound "none", `soundByType` surviving a
* save/load round trip, and `soundFor` preferring a type's override.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
import { beforeEach, describe, expect, it } from "vitest";
import {
NOTIF_KEY,
defaultNotifPrefs,
loadNotifPrefs,
saveNotifPrefs,
soundFor,
} from "../notifPrefs";
beforeEach(() => localStorage.clear());
describe("notifPrefs", () => {
it("defaults to no sound", () => {
expect(loadNotifPrefs()).toEqual(defaultNotifPrefs);
expect(defaultNotifPrefs.sound).toBe("none");
});
it("loads a blob written before sounds existed with sound none", () => {
localStorage.setItem(NOTIF_KEY, JSON.stringify({ enabled: true, onNewSession: true }));
const prefs = loadNotifPrefs();
expect(prefs.sound).toBe("none");
expect(prefs.enabled).toBe(true);
expect(prefs.soundByType).toBeUndefined();
});
it("round-trips soundByType and resolves a type's sound", () => {
saveNotifPrefs({
...defaultNotifPrefs,
sound: "chime",
soundByType: { onTurnComplete: "pop" },
});
const prefs = loadNotifPrefs();
expect(prefs.soundByType).toEqual({ onTurnComplete: "pop" });
expect(soundFor(prefs, "onTurnComplete")).toBe("pop");
expect(soundFor(prefs, "onSessionError")).toBe("chime");
});
});