Модуль: тема и палитра CRM — crm:theme в iframe (data-theme/data-pal), в окно звонка — параметрами ?theme=&pal= с проверкой
This commit is contained in:
@@ -5,6 +5,7 @@ import { CopyButton } from "./CopyButton";
|
|||||||
import { retryReady, useAuth } from "./crm";
|
import { retryReady, useAuth } from "./crm";
|
||||||
import { ConfirmDialog } from "./Dialogs";
|
import { ConfirmDialog } from "./Dialogs";
|
||||||
import { Icon } from "./Icon";
|
import { Icon } from "./Icon";
|
||||||
|
import { withTheme } from "./theme";
|
||||||
|
|
||||||
const WS_KEY = "calls_ws_v1:";
|
const WS_KEY = "calls_ws_v1:";
|
||||||
|
|
||||||
@@ -82,8 +83,9 @@ function Room(props: { ws: string }) {
|
|||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
const r = await api.join(ws);
|
const r = await api.join(ws);
|
||||||
if (w && !w.closed) w.location.href = r.url;
|
const url = withTheme(r.url);
|
||||||
else setFallback(r.url); // браузер заблокировал окно — ссылка вручную (код живёт 60 секунд)
|
if (w && !w.closed) w.location.href = url;
|
||||||
|
else setFallback(url); // браузер заблокировал окно — ссылка вручную (код живёт 60 секунд)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
try { w?.close(); } catch { /* уже закрыто */ }
|
try { w?.close(); } catch { /* уже закрыто */ }
|
||||||
setJoinErr((e as Error).message);
|
setJoinErr((e as Error).message);
|
||||||
|
|||||||
@@ -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 { useSyncExternalStore } from "react";
|
||||||
|
import { applyTheme, parseTheme } from "./theme";
|
||||||
|
|
||||||
export type User = { id: string; email: string | null; name: string | null };
|
export type User = { id: string; email: string | null; name: string | null };
|
||||||
export type CrmWorkspace = { id?: string; kind?: string; title?: string };
|
export type CrmWorkspace = { id?: string; kind?: string; title?: string };
|
||||||
@@ -150,6 +151,9 @@ function handle(ev: MessageEvent) {
|
|||||||
else if (d.type === "crm:workspace") {
|
else if (d.type === "crm:workspace") {
|
||||||
lastWs = d.workspace ?? null;
|
lastWs = d.workspace ?? null;
|
||||||
set({ crmWorkspace: wsKey(lastWs, state.user?.id) });
|
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") {
|
} else if (d.type === "crm:logout") {
|
||||||
token = null; expiresAt = null;
|
token = null; expiresAt = null;
|
||||||
set({ status: "logout", user: undefined });
|
set({ status: "logout", user: undefined });
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import App from "./App";
|
|||||||
import { CallPage } from "./Call";
|
import { CallPage } from "./Call";
|
||||||
import { initCrm } from "./crm";
|
import { initCrm } from "./crm";
|
||||||
import { GuestPage } from "./Guest";
|
import { GuestPage } from "./Guest";
|
||||||
|
import { initCallTheme } from "./theme";
|
||||||
import "./calls.css";
|
import "./calls.css";
|
||||||
|
|
||||||
// Три страницы: / — модуль в iframe CRM, /call — окно звонка, /g/<токен> — гость.
|
// Три страницы: / — модуль в iframe CRM, /call — окно звонка, /g/<токен> — гость.
|
||||||
const path = location.pathname;
|
const path = location.pathname;
|
||||||
const guest = /^\/g\/([A-Za-z0-9_-]{20,100})\/?$/.exec(path);
|
const guest = /^\/g\/([A-Za-z0-9_-]{20,100})\/?$/.exec(path);
|
||||||
let page;
|
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 if (guest) page = <GuestPage token={guest[1]} />;
|
||||||
else { void initCrm(); page = <App />; }
|
else { void initCrm(); page = <App />; }
|
||||||
|
|
||||||
|
|||||||
52
calls/frontend/src/theme.ts
Normal file
52
calls/frontend/src/theme.ts
Normal 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; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user