Модуль: тема и палитра CRM — crm:theme в iframe (data-theme/data-pal), в окно звонка — параметрами ?theme=&pal= с проверкой

This commit is contained in:
EugeneTes
2026-10-15 10:05:52 +00:00
parent e49f5daabf
commit 181b13b4c2
4 changed files with 63 additions and 4 deletions

View File

@@ -5,6 +5,7 @@ import { CopyButton } from "./CopyButton";
import { retryReady, useAuth } from "./crm";
import { ConfirmDialog } from "./Dialogs";
import { Icon } from "./Icon";
import { withTheme } from "./theme";
const WS_KEY = "calls_ws_v1:";
@@ -82,8 +83,9 @@ function Room(props: { ws: string }) {
setBusy(true);
try {
const r = await api.join(ws);
if (w && !w.closed) w.location.href = r.url;
else setFallback(r.url); // браузер заблокировал окно — ссылка вручную (код живёт 60 секунд)
const url = withTheme(r.url);
if (w && !w.closed) w.location.href = url;
else setFallback(url); // браузер заблокировал окно — ссылка вручную (код живёт 60 секунд)
} catch (e) {
try { w?.close(); } catch { /* уже закрыто */ }
setJoinErr((e as Error).message);

View File

@@ -1,6 +1,7 @@
// Мост с CRM: postMessage (crm:ready → crm:auth, crm:workspace, crm:refresh, crm:logout).
// Мост с CRM: postMessage (crm:ready → crm:auth, crm:workspace, crm:theme, crm:refresh, crm:logout).
// Токен живёт только в памяти, никуда не логируется и не выводится.
import { useSyncExternalStore } from "react";
import { applyTheme, parseTheme } from "./theme";
export type User = { id: string; email: string | null; name: string | null };
export type CrmWorkspace = { id?: string; kind?: string; title?: string };
@@ -150,6 +151,9 @@ function handle(ev: MessageEvent) {
else if (d.type === "crm:workspace") {
lastWs = d.workspace ?? null;
set({ crmWorkspace: wsKey(lastWs, state.user?.id) });
} else if (d.type === "crm:theme") {
const t = parseTheme(d.theme, d.palette);
if (t) applyTheme(t);
} else if (d.type === "crm:logout") {
token = null; expiresAt = null;
set({ status: "logout", user: undefined });

View File

@@ -4,13 +4,14 @@ import App from "./App";
import { CallPage } from "./Call";
import { initCrm } from "./crm";
import { GuestPage } from "./Guest";
import { initCallTheme } from "./theme";
import "./calls.css";
// Три страницы: / — модуль в iframe CRM, /call — окно звонка, /g/<токен> — гость.
const path = location.pathname;
const guest = /^\/g\/([A-Za-z0-9_-]{20,100})\/?$/.exec(path);
let page;
if (path === "/call" || path === "/call/") page = <CallPage />;
if (path === "/call" || path === "/call/") { initCallTheme(); page = <CallPage />; }
else if (guest) page = <GuestPage token={guest[1]} />;
else { void initCrm(); page = <App />; }

View File

@@ -0,0 +1,52 @@
// Тема и палитра CRM: <html data-theme="light|dark" data-pal="…">, остальное делают стили tvorymo-ui.css.
// В iframe их присылает CRM (crm:theme), в окно звонка — модуль параметрами ?theme=&pal= в ссылке.
const THEME_KEY = "calls_theme_v1";
export type Theme = { theme: "light" | "dark"; pal: string };
/** Проверка значений: тема только light|dark, палитра — короткое имя из латиницы (иначе classic). */
export function parseTheme(theme: unknown, pal: unknown): Theme | null {
if (theme !== "light" && theme !== "dark") return null;
const p = typeof pal === "string" && /^[a-z][a-z0-9-]{0,31}$/.test(pal) ? pal : "classic";
return { theme, pal: p };
}
export function applyTheme(t: Theme) {
const el = document.documentElement;
el.dataset.theme = t.theme;
el.dataset.pal = t.pal;
}
/** Текущая тема модуля (если CRM её присылала) — для передачи в окно звонка. */
export function currentTheme(): Theme | null {
const el = document.documentElement;
return parseTheme(el.dataset.theme, el.dataset.pal);
}
/** Окно звонка: тема из ссылки, при перезагрузке — из sessionStorage окна. Без неё — системная. */
export function initCallTheme() {
const q = new URLSearchParams(location.search);
let t = parseTheme(q.get("theme"), q.get("pal"));
if (t) {
try { sessionStorage.setItem(THEME_KEY, JSON.stringify(t)); } catch { /* хранилище недоступно */ }
} else {
try {
const s = JSON.parse(sessionStorage.getItem(THEME_KEY) || "null");
t = s && parseTheme(s.theme, s.pal);
} catch { /* битое значение */ }
}
if (t) applyTheme(t);
}
/** Ссылка на окно звонка с темой модуля (в окно crm:theme не приходит). */
export function withTheme(url: string): string {
const t = currentTheme();
if (!t) return url;
try {
const u = new URL(url, location.href);
u.searchParams.set("theme", t.theme);
u.searchParams.set("pal", t.pal);
return u.toString();
} catch { return url; }
}