diff --git a/calls/frontend/src/App.tsx b/calls/frontend/src/App.tsx index bc4d6dc..3dfdd3c 100644 --- a/calls/frontend/src/App.tsx +++ b/calls/frontend/src/App.tsx @@ -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); diff --git a/calls/frontend/src/crm.ts b/calls/frontend/src/crm.ts index 88ffa80..f8f3543 100644 --- a/calls/frontend/src/crm.ts +++ b/calls/frontend/src/crm.ts @@ -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 }); diff --git a/calls/frontend/src/main.tsx b/calls/frontend/src/main.tsx index 19737b1..0587fe8 100644 --- a/calls/frontend/src/main.tsx +++ b/calls/frontend/src/main.tsx @@ -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 = ; +if (path === "/call" || path === "/call/") { initCallTheme(); page = ; } else if (guest) page = ; else { void initCrm(); page = ; } diff --git a/calls/frontend/src/theme.ts b/calls/frontend/src/theme.ts new file mode 100644 index 0000000..8f10b3f --- /dev/null +++ b/calls/frontend/src/theme.ts @@ -0,0 +1,52 @@ +// Тема и палитра CRM: , остальное делают стили 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; } +}