Files
roboco/panel/src/lib/telegram/theme.ts
T
3c5ee46347 feat(tg): Mini App V6 — premium overhaul (#609)
* feat(tg): Mini App V6 — premium overhaul (design system, Chat parity, Metrics drilldown, CEO verbs)

Design system: native type with tabular-numeral heroes (mono demoted to
the wordmark), borderless elevated cards, floating dock, Telegram
window-chrome painting via the theme bridge; Inbox moves behind a header
bell with humanized notifications (UUIDs resolve to task names).

Chat: honest Mine/Fleet split — participant-scoped CEO threads with real
unread counts and mark-read, watched fleet threads with reply-as-CEO on
task-linked conversations (watch-only otherwise), markdown transcripts,
live pulse flashes, and a pinned Secretary live chat on the panel's SSE
session runtime.

Metrics: new tab with period-segmented spend hero, by-agent/team/model
breakdowns, delivery + efficiency health, and a per-agent drilldown over
usage time-series (agent_slug) + member scorecard.

Board: tg-native grouped pipeline replacing the MobileTaskBoard wrapper;
task sheet gains the CEO decide verbs (approve / request changes /
unblock).

Security: /api/dashboard router now require_panel_token-gated at router
level (mirrors /api/usage), closing unauthenticated metrics exposure.

* fix(tg): restore Share Tech Mono brand voice, Phosphor icon set, borderless avatars

The mono returns as the numeral/brand voice (.tg-display — heroes, stat
values, wordmark) while labels stay native sentence case. The hand-drawn
duotone glyphs and lucide feature icons are replaced by Phosphor (MIT):
duotone at rest via an IconContext at the shell, filled weight on the
dock's active tab; row glyph maps (board statuses, inbox kinds, approval
kinds, quick actions) all move over. Team avatar tiles drop their borders
— tint-only squircles.

* fix(tg): fleet avatar strip breathes — spaced tiles instead of overlap

* polish(tg): taste-skill audit pass — em-dash purge, one icon family, separator rationing

Applied the design-taste audit against the cockpit: every em-dash in
visible UI copy rewritten (periods/commas/colons), the remaining lucide
chrome (carets, arrows, send, close, spinners) moved to Phosphor so the
tg tree ships one icon family (send is the native paper-plane, carets
bold), the hand-rolled chevron SVG deleted, and metadata lines rationed
to a single middle-dot separator.

* polish(tg): pipeline chip strip scrolls without a visible scrollbar

* fix(tests): metrics observability fixture uses a relative timestamp

The hardcoded _T0 (2026-06-20) aged out of the service's 30-day window
exactly 30 days later, detonating the suite on every branch. Two days
back from now() stays inside every window (30d metrics, 7d scorecards)
permanently.

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
2026-07-20 17:29:22 +02:00

92 lines
3.7 KiB
TypeScript

/**
* 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);
}