Files
roboco/panel/src/lib/telegram/theme.ts
T

92 lines
3.7 KiB
TypeScript
Raw Normal View History

/**
* Telegram theme → panel token bridge.
*
* Maps the launcher's `themeParams` colors onto the shadcn CSS variables the
* whole component library reads, scoped to the `(tg)` shell element only —
* the desktop dashboard keeps its own theme. Later map entries win by
* re-setting the same variable, so a newer, more specific Telegram key
* (e.g. `section_bg_color`) overrides the broader fallback before it.
*/
import type { TelegramThemeParams, TelegramWebApp } from "./webapp";
/** [Telegram key, panel CSS variable] — applied in order. Surfaces only:
* the accent tokens (--primary/--ring) deliberately stay RoboCo's amber
* (the #tg-shell skin in globals.css) so the cockpit keeps its identity
* inside any Telegram theme — Telegram's surfaces, RoboCo's voice. */
const THEME_VAR_MAP: ReadonlyArray<[keyof TelegramThemeParams, string]> = [
["bg_color", "--background"],
["secondary_bg_color", "--background"],
["bg_color", "--card"],
["section_bg_color", "--card"],
["bg_color", "--popover"],
["section_bg_color", "--popover"],
["text_color", "--foreground"],
["text_color", "--card-foreground"],
["text_color", "--popover-foreground"],
["hint_color", "--muted-foreground"],
["subtitle_text_color", "--muted-foreground"],
["destructive_text_color", "--destructive"],
["section_separator_color", "--border"],
];
/** Telegram promises `#rrggbb`; anything else is dropped rather than
* injected into a style attribute (the bridge object is still a trust
* boundary — a malformed value must not become CSS). */
const HEX_COLOR = /^#[0-9a-f]{6}$/i;
/**
* Applies the WebApp's current colorScheme + themeParams to `root`: toggles
* the `.dark` class (so unmapped tokens and `dark:` variants stay coherent)
* and sets every validly-colored mapped variable inline (inline wins over
* both `:root` and `.dark` definitions). Missing/invalid params are simply
* skipped — the panel's own theme shows through, which is the right
* degraded look.
*/
/** The #tg-shell default background as hex — what Telegram's own window
* chrome is painted with when the theme doesn't hand us a bg_color.
* Keep in step with `--background` in globals.css' #tg-shell block. */
const SHELL_BG_HEX = "#14171c";
export function applyTelegramTheme(
webApp: TelegramWebApp,
root: HTMLElement,
): void {
root.classList.toggle("dark", webApp.colorScheme === "dark");
const params = webApp.themeParams ?? {};
for (const [key, cssVar] of THEME_VAR_MAP) {
const value = params[key];
if (value && HEX_COLOR.test(value)) {
root.style.setProperty(cssVar, value);
}
}
// Paint Telegram's own window chrome (titlebar / app bg / bottom bar) to
// the shell background so the cockpit blends edge-to-edge into the client
// instead of sitting framed inside default chrome — the single biggest
// "native app, not website" tell.
const bg =
params.bg_color && HEX_COLOR.test(params.bg_color)
? params.bg_color
: SHELL_BG_HEX;
webApp.setHeaderColor?.(bg);
webApp.setBackgroundColor?.(bg);
webApp.setBottomBarColor?.(bg);
}
/**
* Applies the theme now and re-applies on every `themeChanged` bridge event
* (the user switching Telegram themes mid-session). Returns a cleanup that
* unsubscribes; safe when the bridge lacks onEvent/offEvent (older clients,
* the dev mock) — then it's a one-shot apply with a no-op cleanup.
*/
export function startTelegramThemeSync(
webApp: TelegramWebApp,
root: HTMLElement,
): () => void {
applyTelegramTheme(webApp, root);
if (!webApp.onEvent || !webApp.offEvent) return () => undefined;
const handler = () => applyTelegramTheme(webApp, root);
webApp.onEvent("themeChanged", handler);
return () => webApp.offEvent?.("themeChanged", handler);
}