2026-07-19 07:29:04 +02:00
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
2026-07-20 17:29:22 +02:00
|
|
|
/** 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";
|
|
|
|
|
|
2026-07-19 07:29:04 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-20 17:29:22 +02:00
|
|
|
// 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);
|
2026-07-19 07:29:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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);
|
|
|
|
|
}
|