Files
ihasmail-inbuxa/web/src/views/settings/AppearanceSettings.tsx
T
jcoffey-dev c3cecf9916 Let messages follow the app theme, at the user's choice
Messages render on a white card in every theme. That is deliberate for mail
that styles itself, but #4 points out the case it gets wrong: a message with
no styling of its own has nothing worth preserving, and flashing white at
someone reading in the dark is a real cost.

Appearance gains a switch under the theme cards, off by default so the
current behaviour is unchanged. With it on, HTML mail that declares no
colours follows the app theme; mail that sets a background or text colour
still gets the light card it was designed for, because half-darkening someone
else's design is worse than leaving it alone. Plain-text mail already
followed the theme and is untouched by the switch.

The themed palette is expressed in the app's own custom properties, which
cross the shadow boundary, so switching theme repaints open messages without
re-rendering them, and the accent-coloured link stays consistent. The host
element takes color-scheme: inherit so form controls and scrollbars inside a
message match too.

htmlDeclaresColors covers bgcolor attributes, <font color>, and colour or
background declarations in style attributes and <style> blocks, while
ignoring near-misses like border-color and ?color= in a URL.

Closes #4
2026-08-23 13:34:24 -07:00

68 lines
3.0 KiB
TypeScript

import { useSettings } from "@/store/settings";
import { Switch } from "@/ui/misc";
const ACCENTS = [
{ id: "teal", color: "#0f766e" },
{ id: "blue", color: "#2563eb" },
{ id: "purple", color: "#7c3aed" },
{ id: "rose", color: "#e11d48" },
{ id: "orange", color: "#ea580c" },
{ id: "green", color: "#16a34a" },
];
export function AppearanceSettings() {
const s = useSettings((st) => st.settings);
const update = useSettings((st) => st.update);
return (
<div>
<h1>Appearance</h1>
<p className="lead">Make ihasmail yours.</p>
<h2>Theme</h2>
<div className="theme-grid">
{(["system", "light", "dark"] as const).map((t) => (
<button key={t} className={`theme-card ${s.theme === t ? "active" : ""}`} onClick={() => update({ theme: t })}>
<div className="preview" style={{ background: t === "dark" ? "#0b1220" : t === "light" ? "#f6f8fa" : "linear-gradient(90deg,#f6f8fa 50%,#0b1220 50%)" }} />
{t === "system" ? "Match system" : t === "light" ? "Light" : "Dark"}
</button>
))}
</div>
<Switch
checked={s.themeMessageBody}
onChange={(v) => update({ themeMessageBody: v })}
label="Apply the theme to messages too"
hint="Plain-text mail already follows the theme. With this on, HTML mail that brings no colours of its own does as well, instead of sitting on a white card. Messages that style themselves are left exactly as the sender designed them."
/>
<h2>Accent color</h2>
<div className="swatches">
{ACCENTS.map((a) => (
<button key={a.id} className={`swatch ${s.accent === a.id ? "active" : ""}`} style={{ background: a.color }} onClick={() => update({ accent: a.id })} aria-label={a.id} title={a.id} />
))}
</div>
<h2>Density & text</h2>
<div className="field-row">
<div className="field">
<label>Display density</label>
<select className="select" value={s.density} onChange={(e) => update({ density: e.target.value as typeof s.density })}>
<option value="comfortable">Comfortable</option>
<option value="cozy">Cozy (default)</option>
<option value="compact">Compact</option>
</select>
</div>
<div className="field">
<label>Text size</label>
<select className="select" value={s.fontSize} onChange={(e) => update({ fontSize: e.target.value as typeof s.fontSize })}>
<option value="small">Small</option>
<option value="medium">Medium</option>
<option value="large">Large</option>
</select>
</div>
</div>
<h2>Sidebar</h2>
<Switch checked={s.labelsSidebar} onChange={(v) => update({ labelsSidebar: v })} label="Show labels in the sidebar" />
<Switch checked={s.showHiddenFolders} onChange={(v) => update({ showHiddenFolders: v })} label="Show unsubscribed (hidden) folders" />
<Switch checked={s.sidebarCollapsed} onChange={(v) => update({ sidebarCollapsed: v })} label="Collapse sidebar to icons" />
</div>
);
}