Files
native/src/widgets/theme.ts
T

177 lines
5.1 KiB
TypeScript

// Home-screen widget palettes. These copy the webmail's standard theme as it
// renders (app/globals.css plus the Tailwind classes the list, calendar and
// sidebar use), not the in-app ThemePalette: in the webmail's dark mode the
// primary colour is white, while the native dark palette keeps a blue primary.
// Widgets are meant to look like the webmail, so they carry their own tokens.
import type { ColorProp } from 'react-native-android-widget';
export type WidgetScheme = 'light' | 'dark';
export interface WidgetPalette {
scheme: WidgetScheme;
/**
* Widget surface (webmail `--color-background`). The edge around it is drawn
* natively, as `widget_frame` in android/app/src/main/res/values/colors.xml.
*/
bg: ColorProp;
fg: ColorProp;
muted: ColorProp;
/** Search pill, segmented track, quota ring track (webmail `--color-muted`). */
mutedBg: ColorProp;
border: ColorProp;
primary: ColorProp;
primaryFg: ColorProp;
accent: ColorProp;
accentFg: ColorProp;
/** `bg-accent/30`, the tint of an unread message row. */
unreadRow: ColorProp;
unreadDot: ColorProp;
/** Agenda day-header band. */
dayHeader: ColorProp;
/** "Today" label in the agenda. */
today: ColorProp;
/** Read subject (`text-foreground/90`). */
subjectRead: ColorProp;
success: ColorProp;
successText: ColorProp;
successBg: ColorProp;
info: ColorProp;
infoBg: ColorProp;
destructive: ColorProp;
destructiveBg: ColorProp;
overdue: ColorProp;
dueToday: ColorProp;
star: ColorProp;
/** Outline button edge (webmail `--color-input`). */
input: ColorProp;
/** Scheduled-send label (`sky-500/10` pill with `sky-700/300` text). */
scheduledBg: ColorProp;
scheduledText: ColorProp;
warningBg: ColorProp;
warningText: ColorProp;
folder: {
inbox: ColorProp;
drafts: ColorProp;
sent: ColorProp;
scheduled: ColorProp;
archive: ColorProp;
junk: ColorProp;
};
}
export const LIGHT: WidgetPalette = {
scheme: 'light',
bg: '#ffffff',
fg: '#0f172a',
muted: '#64748b',
mutedBg: '#f1f5f9',
border: '#e2e8f0',
primary: '#3b82f6',
primaryFg: '#ffffff',
accent: '#dbeafe',
accentFg: '#1e40af',
unreadRow: '#f4f9ff',
unreadDot: '#3b82f6',
dayHeader: '#f4f7fa',
today: '#3b82f6',
subjectRead: '#283548',
success: '#22c55e',
successText: '#15803d',
successBg: 'rgba(34, 197, 94, 0.15)',
info: '#2563eb',
infoBg: 'rgba(59, 130, 246, 0.15)',
destructive: '#ef4444',
destructiveBg: 'rgba(239, 68, 68, 0.15)',
overdue: '#e7000b',
dueToday: '#155dfc',
star: '#fbbf24',
input: '#e2e8f0',
scheduledBg: 'rgba(0, 166, 244, 0.1)',
scheduledText: '#0069a8',
warningBg: 'rgba(254, 154, 0, 0.12)',
warningText: '#973c00',
folder: {
inbox: '#155dfccc',
drafts: '#7f22fecc',
sent: '#009966cc',
scheduled: '#0084d1',
archive: '#e17100cc',
junk: '#e7000bcc',
},
};
export const DARK: WidgetPalette = {
scheme: 'dark',
bg: '#0a0a0a',
fg: '#fafafa',
muted: '#a3a3a3',
mutedBg: '#262626',
border: '#2d2d2d',
primary: '#fafafa',
primaryFg: '#171717',
accent: '#1e3a8a',
accentFg: '#dbeafe',
unreadRow: '#0f1830',
unreadDot: '#60a5fa',
dayHeader: '#202020',
today: '#fafafa',
subjectRead: '#e1e1e1',
success: '#16a34a',
successText: '#22c55e',
successBg: 'rgba(22, 163, 74, 0.15)',
info: '#60a5fa',
infoBg: 'rgba(96, 165, 250, 0.15)',
destructive: '#ef4444',
destructiveBg: 'rgba(239, 68, 68, 0.15)',
overdue: '#ff6467',
dueToday: '#51a2ff',
star: '#fbbf24',
input: '#262626',
scheduledBg: 'rgba(0, 166, 244, 0.1)',
scheduledText: '#74d4ff',
warningBg: 'rgba(254, 154, 0, 0.12)',
warningText: '#ffd230',
folder: {
inbox: '#51a2ffcc',
drafts: '#a684ffcc',
sent: '#00d492cc',
scheduled: '#00bcff',
archive: '#ffb900cc',
junk: '#ff6467cc',
},
};
/** Tint a calendar colour the way event chips do (`${color}24`, ~14% alpha). */
export function chipBackground(color: string): ColorProp {
const hex = normalizeHex(color);
return `${hex}24` as ColorProp;
}
/** Accepts `#rgb`, `#rrggbb`, `hsl(h, s%, l%)`; anything else falls back to blue. */
export function normalizeHex(color: string | null | undefined): `#${string}` {
if (!color) return '#3b82f6';
const c = color.trim();
if (/^#[0-9a-f]{6}$/i.test(c)) return c.toLowerCase() as `#${string}`;
if (/^#[0-9a-f]{3}$/i.test(c)) {
const [r, g, b] = c.slice(1).split('');
return `#${r}${r}${g}${g}${b}${b}`.toLowerCase() as `#${string}`;
}
if (/^#[0-9a-f]{8}$/i.test(c)) return c.slice(0, 7).toLowerCase() as `#${string}`;
const hsl = /^hsla?\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%/i.exec(c);
if (hsl) return hslToHex(Number(hsl[1]), Number(hsl[2]), Number(hsl[3]));
return '#3b82f6';
}
export function hslToHex(h: number, s: number, l: number): `#${string}` {
const sat = s / 100;
const light = l / 100;
const k = (n: number) => (n + h / 30) % 12;
const a = sat * Math.min(light, 1 - light);
const f = (n: number) => light - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
const hex = [f(0), f(8), f(4)]
.map((x) => Math.round(x * 255).toString(16).padStart(2, '0'))
.join('');
return `#${hex}`;
}