diff --git a/kez-chat/web/index.html b/kez-chat/web/index.html index fad9c0d..b729c7f 100644 --- a/kez-chat/web/index.html +++ b/kez-chat/web/index.html @@ -29,6 +29,24 @@ + + +
diff --git a/kez-chat/web/src/app.css b/kez-chat/web/src/app.css index 7ca8b05..d1b3b7d 100644 --- a/kez-chat/web/src/app.css +++ b/kez-chat/web/src/app.css @@ -49,6 +49,31 @@ } /* ─── Base ─────────────────────────────────────────────────────────────── */ +/* Light theme — overrides the dark @theme token values. These utilities + all compile to var(--color-*), so flipping the variable values flips + the whole app. Unlayered + after @theme, so it wins regardless of + specificity. An inline script in index.html sets data-theme before + paint to avoid a flash; the Settings toggle updates it at runtime. */ +:root[data-theme="light"] { + color-scheme: light; + --color-bg: #f6f7f9; + --color-surface: #ffffff; + --color-elevated: #eef0f3; + --color-border: #dce0e6; + --color-text: #15181d; + --color-text-secondary: #5a626d; + --color-text-muted: #8b929c; + --color-text-disabled: #b8bdc5; + /* Deeper cyan so accent-as-text is legible on white; fills still read cyan. */ + --color-accent: #0e90b4; + --color-accent-dim: #0a7290; + --color-accent-contrast: #ffffff; + --color-verified: #15a34a; + --color-danger: #dc2626; + --color-warning: #c2700a; + --color-bubble-recv: #eceff3; +} + :root { font-family: var(--font-sans); color: var(--color-text); diff --git a/kez-chat/web/src/lib/EmojiButton.svelte b/kez-chat/web/src/lib/EmojiButton.svelte index 11ffd78..c614419 100644 --- a/kez-chat/web/src/lib/EmojiButton.svelte +++ b/kez-chat/web/src/lib/EmojiButton.svelte @@ -5,6 +5,7 @@ // clicks are instant (module cached). import { onDestroy } from "svelte"; + import { theme } from "./theme.svelte.js"; interface Props { /** Called with the picked emoji's character. */ @@ -34,8 +35,8 @@ await import("emoji-picker-element"); pickerEl = document.createElement("emoji-picker") as HTMLElement; pickerEl.addEventListener("emoji-click", onEmojiClick as EventListener); - // Match the dark-on-light scheme of the rest of the app. - pickerEl.classList.add("light"); + // Follow the app theme (emoji-picker-element honors .light/.dark). + pickerEl.classList.add(theme.effective); } catch (e) { loadError = (e as Error).message; } finally { @@ -116,7 +117,7 @@ is themeable here. Width comes from the component itself (~340px). */ :global(emoji-picker) { height: 360px; - --background: #fff; - --border-color: #e5e7eb; + --background: var(--color-elevated); + --border-color: var(--color-border); } diff --git a/kez-chat/web/src/lib/theme.svelte.ts b/kez-chat/web/src/lib/theme.svelte.ts new file mode 100644 index 0000000..e5999a9 --- /dev/null +++ b/kez-chat/web/src/lib/theme.svelte.ts @@ -0,0 +1,73 @@ +// Light / dark / system theme switching. +// +// The CSS lives in app.css: dark is the @theme default, and a +// :root[data-theme="light"] block overrides the token values. This +// module just decides which data-theme attribute to set on and +// persists the user's choice. +// +// "system" follows prefers-color-scheme live (re-applies when the OS +// flips). An inline script in index.html sets the initial attribute +// before first paint to avoid a flash of the wrong theme. + +export type ThemeChoice = "light" | "dark" | "system"; + +const KEY = "kez-chat:theme"; + +function systemPrefersLight(): boolean { + return ( + typeof matchMedia !== "undefined" && + matchMedia("(prefers-color-scheme: light)").matches + ); +} + +function effectiveOf(choice: ThemeChoice): "light" | "dark" { + if (choice === "system") return systemPrefersLight() ? "light" : "dark"; + return choice; +} + +function loadChoice(): ThemeChoice { + const v = (typeof localStorage !== "undefined" && localStorage.getItem(KEY)) || ""; + return v === "light" || v === "dark" || v === "system" ? v : "system"; +} + +class ThemeStore { + choice = $state+ {#if theme.choice === "system"} + Following your device ({theme.effective}). Change your OS setting and it tracks automatically. + {:else} + Always {theme.choice}. + {/if} +
+