From 42d9ccfbf3a2b6934329ec670b655970824bae14 Mon Sep 17 00:00:00 2001 From: heaven Date: Tue, 5 May 2026 19:59:24 +0300 Subject: [PATCH] feat(bots): unify command-card chrome with left-side semantic icons and fold WA Meta-ToS warning into the AboutCard modal --- apps/widget-discord/src/App.tsx | 66 +++++++- apps/widget-discord/src/styles.css | 24 ++- apps/widget-telegram/src/App.tsx | 70 ++++++++- apps/widget-telegram/src/styles.css | 32 +++- apps/widget-whatsapp/src/App.tsx | 233 ++++++++++++---------------- apps/widget-whatsapp/src/i18n/en.ts | 10 +- apps/widget-whatsapp/src/i18n/ru.ts | 62 +++----- apps/widget-whatsapp/src/styles.css | 122 ++++++++------- 8 files changed, 375 insertions(+), 244 deletions(-) diff --git a/apps/widget-discord/src/App.tsx b/apps/widget-discord/src/App.tsx index 0743747f..68924b16 100644 --- a/apps/widget-discord/src/App.tsx +++ b/apps/widget-discord/src/App.tsx @@ -46,6 +46,55 @@ const RefreshIcon = () => ( ); +// Inline SVG info icon — leads the AboutCard. Shared shape with the +// Telegram widget. WhatsApp uses a triangle warning glyph instead +// because its About modal also carries a Meta-ToS risk disclosure. +const InfoIcon = () => ( + +); + +// Three QR finder squares + a few module dots — leads the QR-login +// card. Same shape across all three bot widgets. +const QrIcon = () => ( + +); + +// Sign-out arrow leaving an open box — leads the destructive logout +// card. Open right side conveys «out of the session». Stays muted +// inside `.command-card.danger` so the rose accent remains a single +// accent on the title. +const LogoutIcon = () => ( + +); + +// Two chain links joined by a horizontal bar — leads the Discord- +// only Reconnect card. Visually distinct from RefreshIcon's circular +// arrows so the user can tell «re-establish bridge link» from +// «re-fetch status» at a glance. +const LinkIcon = () => ( + +); + // Linkifier — same heuristic as TG widget. const URL_RE = /https?:\/\/[^\s)]+/g; @@ -281,6 +330,9 @@ type AboutCardProps = { const AboutCard = ({ t, onOpen }: AboutCardProps) => ( setAboutOpen(true)} /> @@ -946,6 +1007,9 @@ export function App({ bootstrap, api }: Props) { * the same command-card chrome so it visually matches Login / * Logout cards. */} setAboutOpen(true)} /> diff --git a/apps/widget-telegram/src/styles.css b/apps/widget-telegram/src/styles.css index 1cce97bd..ca46dbaa 100644 --- a/apps/widget-telegram/src/styles.css +++ b/apps/widget-telegram/src/styles.css @@ -319,11 +319,33 @@ body { display: block; } -/* Spin the chevron icon while the card is in its `refreshing` in-flight - * state. Combined with `disabled` (which dims the card to opacity 0.5 and - * gates :hover via :not(:disabled)), the spinner is the unambiguous «I'm - * working» signal — no more guessing whether the click registered. */ -.command-card.refreshing .command-card-chevron svg { +/* Generic leading-icon slot — every command-card carries a semantic + * left-side glyph (mirror of the right-side chevron). Picks up + * `currentColor` from the parent and stays muted by default; the + * `.danger` modifier on logout deliberately does NOT colour the lead + * icon so the rose accent stays reserved for the title (one accent + * per card). */ +.command-card-lead-icon { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--muted); +} +.command-card-lead-icon svg { + width: 20px; + height: 20px; + display: block; +} + +/* Spin the leading refresh icon while the card is in its `refreshing` + * in-flight state. Combined with `disabled` (which dims the card to + * opacity 0.5 and gates :hover via :not(:disabled)), the spinner is + * the unambiguous «I'm working» signal — no more guessing whether the + * click registered. The selector targets the lead slot since the + * refresh card moved its glyph from the chevron (right) to the lead + * slot (left) for parity with every other card. */ +.command-card.refreshing .command-card-lead-icon svg { animation: command-card-spin 0.8s linear infinite; } @keyframes command-card-spin { diff --git a/apps/widget-whatsapp/src/App.tsx b/apps/widget-whatsapp/src/App.tsx index 21726cb9..a358f619 100644 --- a/apps/widget-whatsapp/src/App.tsx +++ b/apps/widget-whatsapp/src/App.tsx @@ -55,6 +55,59 @@ const RefreshIcon = () => ( ); +// Smartphone outline — leads the «Войти по номеру» card. Shared shape +// across TG (text-login) and WhatsApp (pairing-code login) since both +// flows boil down to «вы вводите номер с телефона». +const PhoneIcon = () => ( + +); + +// Three QR finder squares + a few module dots — leads every QR-login +// card. The finder pattern is the strongest «this is QR» visual cue; +// no need to draw a full code. +const QrIcon = () => ( + +); + +// Sign-out arrow leaving an open box — leads the destructive logout +// card. Open right side conveys «out of the session». Picks up the +// rose tint via `.command-card.danger` cascade only on the title; +// the lead icon stays muted so the rose stays a single accent. +const LogoutIcon = () => ( + +); + +// Triangle warning glyph — leads the WhatsApp-only AboutCard (which +// carries `command-card warn` for the amber outline) and re-appears +// inside the AboutModal's risk-disclosure callout. Stroke-only so it +// picks up the amber tint via `currentColor` in either context. +const WarningIcon = () => ( + +); + // Linkifier — same heuristic as TG / Discord widgets. const URL_RE = /https?:\/\/[^\s)]+/g; @@ -583,8 +636,17 @@ type AboutCardProps = { onOpen: () => void; }; +// WhatsApp-only: AboutCard carries the `warn` modifier so the amber +// border + amber-tinted name signal that the modal behind it includes +// the Meta-ToS risk disclosure. The leading icon is the triangle +// WarningIcon (not the info-circle used by TG / Discord) for the +// same reason — the hybrid card description («о работе и рисках») +// stays believable when the icon previews the «risks» half. const AboutCard = ({ t, onOpen }: AboutCardProps) => ( -
- {/* WhatsApp-specific callout: surfaces the Meta-ToS ban risk - * for users who open the About modal without clicking the - * dedicated warning card. Shorter than the warning modal — - * just a heads-up + pointer to the full text. */} -

{t('warning.about-callout')}

+ {/* WhatsApp-specific Meta-ToS account-ban risk disclosure. + * Lives at the top of the About modal as an amber-tinted + * block — same content the dedicated WarningCard used to + * carry on the disconnected screen, folded in here so the + * About card is the single info entry point (matches the + * TG / Discord shape; the amber block carries the «risks» + * half of the hybrid card description). */} +

{t('about.body-1')}

{t('about.body-2')}

{t('about.body-3')}

@@ -658,121 +738,6 @@ const AboutModal = ({ t, onClose }: AboutModalProps) => { ); }; -// -------------------------------------------------------------------------- -// Warning card + modal (WhatsApp-specific Meta-ToS risk disclosure) -// -------------------------------------------------------------------------- -// -// WhatsApp is the only bot in the Vojo catalog that carries a real -// account-loss risk for the user. Meta's WhatsApp ToS forbids -// connecting an account through unofficial clients (mautrix-whatsapp -// uses the same multi-device API as WhatsApp Web — technically -// standard, but Meta may treat it as a violation). Enforcement is -// unpredictable. Users who don't understand this risk before clicking -// «Войти» can lose their primary messenger. -// -// Two surfaces: -// 1. A dedicated card on the disconnected screen above the login -// cards. Amber tone, ⚠ glyph in the title — visible even if the -// user skips the About modal. -// 2. A short callout at the top of the About modal (handled in -// AboutModal above) so users who open About also see it. - -// Inline triangle warning glyph. Stroke-only, picks up `currentColor` -// from the parent so it tints with the amber accent on the card. -const WarningIcon = () => ( - -); - -type WarningCardProps = { - t: T; - onOpen: () => void; -}; - -const WarningCard = ({ t, onOpen }: WarningCardProps) => ( - -); - -type WarningModalProps = { - t: T; - onClose: () => void; -}; - -const WarningModal = ({ t, onClose }: WarningModalProps) => { - // Same Escape + backdrop-click pattern as AboutModal — no focus-trap - // library because the surface is small. The visible header glyph - // makes the modal's purpose unambiguous. - useEffect(() => { - const onKey = (e: KeyboardEvent) => { - if (e.key === 'Escape') onClose(); - }; - window.addEventListener('keydown', onKey); - return () => window.removeEventListener('keydown', onKey); - }, [onClose]); - - return ( - - ); -}; - // -------------------------------------------------------------------------- // Logout card with confirm-in-place // -------------------------------------------------------------------------- @@ -833,6 +798,9 @@ const LogoutCard = ({ loginId, t, onConfirm }: LogoutCardProps) => { disabled={!loginId} title={!loginId ? t('card.logout.gated') : undefined} > +
{t('card.logout.name')}
{t('card.logout.desc')}
@@ -853,7 +821,6 @@ export function App({ bootstrap, api }: Props) { const [transcript, setTranscript] = useState([]); const [handshakeOk, setHandshakeOk] = useState(false); const [aboutOpen, setAboutOpen] = useState(false); - const [warningOpen, setWarningOpen] = useState(false); const [refreshing, setRefreshing] = useState(false); const seenEventIds = useRef(new Set()); const [state, dispatch] = useReducer(loginReducer, initialLoginState); @@ -1313,17 +1280,18 @@ export function App({ bootstrap, api }: Props) { {t('status.disconnected')}
- {/* Warning card sits FIRST. Sized like the other cards - * (no full-width spanning) so the disconnected screen - * reads as a single uniform grid; the amber tint + ⚠ - * icon carry the visual emphasis instead. */} - setWarningOpen(true)} /> {/* Login order mirrors the Telegram widget: phone-flow * («Войти по номеру») first, QR second. Both are valid * primary paths; phone-flow is the more familiar entry * point for users coming from Telegram or used to - * SMS-style codes, so it leads. */} + * SMS-style codes, so it leads. The Meta-ToS risk + * disclosure now lives inside the AboutCard modal as + * an amber callout — the hybrid card description tells + * the user it's there. */} setAboutOpen(true)} /> @@ -1461,7 +1435,6 @@ export function App({ bootstrap, api }: Props) { ) : null} {aboutOpen ? setAboutOpen(false)} /> : null} - {warningOpen ? setWarningOpen(false)} /> : null}
diff --git a/apps/widget-whatsapp/src/i18n/en.ts b/apps/widget-whatsapp/src/i18n/en.ts index 14659f1b..af6f8a3f 100644 --- a/apps/widget-whatsapp/src/i18n/en.ts +++ b/apps/widget-whatsapp/src/i18n/en.ts @@ -20,21 +20,15 @@ export const EN: Record = { 'card.refresh.name': 'Refresh status', 'card.refresh.desc': 'Re-check whether WhatsApp is linked', 'card.refresh.in-flight': 'Checking…', - 'card.warning.name': 'Read before linking', - 'card.warning.desc': 'Important information about risks — tap to open', - 'warning.title': 'Read before linking WhatsApp', + 'warning.title': 'Important before linking WhatsApp', 'warning.body-1': 'Mautrix-whatsapp connects to your account through the same linked-device mechanism as WhatsApp Web. Technically a standard API — but unlike other messengers, WhatsApp’s terms of service explicitly forbid connecting through third-party clients, and Meta may ban your account for it.', 'warning.body-2': 'WhatsApp bans are regular and unpredictable — Meta does not publish criteria. For some users the bridge works for years without issue; for others the account is banned within hours of linking.', 'warning.tos-label': 'WhatsApp terms of service:', 'warning.tos-url': 'https://www.whatsapp.com/legal/terms-of-service', - 'warning.close': 'Got it', - 'warning.aria-close': 'Close warning', - 'warning.about-callout': - '⚠ Before linking WhatsApp, read the “Read before linking” card on the bot’s main screen.', 'card.about.name': 'How the WhatsApp bot works', - 'card.about.desc': 'Sign-in, safety, and source code', + 'card.about.desc': 'How it works and the risks — tap to read', 'about.title': 'About the WhatsApp bot', 'about.body-1': 'This bot connects WhatsApp to Vojo. After sign-in, your private chats and groups from WhatsApp will appear in Vojo’s chat list, and replies from the Vojo app will be sent to your contacts as normal WhatsApp messages.', diff --git a/apps/widget-whatsapp/src/i18n/ru.ts b/apps/widget-whatsapp/src/i18n/ru.ts index c8ef490a..04a2d54e 100644 --- a/apps/widget-whatsapp/src/i18n/ru.ts +++ b/apps/widget-whatsapp/src/i18n/ru.ts @@ -35,40 +35,27 @@ export const RU = { 'card.refresh.name': 'Обновить статус', 'card.refresh.desc': 'Перепроверить, привязан ли WhatsApp', 'card.refresh.in-flight': 'Проверяю…', - // --- Warning card (WhatsApp-specific) --------------------------------- - // Карточка-предупреждение ставится ТОЛЬКО для WhatsApp в каталоге - // Vojo, потому что у WhatsApp ToS прямо запрещает подключение - // через сторонние клиенты и Meta это активно энфорсит. Сравнения с - // Telegram/Discord В ИНТЕРФЕЙСЕ намеренно НЕТ: Telegram user ToS - // (telegram.org/tos) такого ограничения вообще не упоминает, а - // Discord — отдельный кейс. Делать сравнение в копии = вводить - // юзера в заблуждение, поэтому warning-модалка говорит ТОЛЬКО про - // WhatsApp/Meta и ссылается ТОЛЬКО на Meta ToS. + // --- About panel ------------------------------------------------------- + // WhatsApp-only Meta-ToS risk disclosure is folded into the About + // modal as an amber callout at the top of the body. The AboutCard + // itself carries `command-card warn` (amber border + amber name) + // and a triangle warning glyph in the lead slot — instead of the + // info-circle TG / Discord use — so the «риски» half of the hybrid + // description («о работе и рисках») is visible at a glance before + // the user opens the modal. TG / Discord get the plain «вход, + // безопасность, исходный код» variant because they don't carry an + // account-loss risk in the same way (Telegram user ToS doesn't + // forbid third-party clients; Discord's restriction on self-bots + // lives in developer policies, not user ToS proper). The amber + // block keeps the unique WhatsApp framing without claiming anything + // about TG / Discord by comparison. // - // ToS reference: https://www.whatsapp.com/legal/terms-of-service - // секция «Harm To WhatsApp Or Our Users» запрещает «software or - // APIs that function substantially the same as our Services» и + // ToS reference for the body: https://www.whatsapp.com/legal/terms-of-service + // section «Harm To WhatsApp Or Our Users» forbids «software or + // APIs that function substantially the same as our Services» and // «accounts for our Services through unauthorized or automated // means». - // Triangle glyph lives in the icon slot to the LEFT — don't repeat - // it in the title text or it doubles up («⚠ ⚠ Прочтите...»). - 'card.warning.name': 'Прочтите перед подключением', - // Two-line desc: hint + explicit «click here to read» so the user - // doesn't stare at the card wondering if it's interactive (the - // amber tone helps signal it's special, but doesn't tell you it's - // a button). - 'card.warning.desc': 'Важная информация о рисках — нажмите, чтобы открыть', 'warning.title': 'Важно знать до подключения WhatsApp', - // Два информационных параграфа: что технически делает мост и - // почему это под риском, и насколько риск реален. Сравнение - // с другими мессенджерами оставлено НЕЯВНЫМ («в отличие от других - // мессенджеров») — без явного перечисления TG/Discord, потому что - // у Telegram user ToS (telegram.org/tos) запрета на сторонние - // клиенты нет вообще, у Discord ToS тоже нет (запрет self-bot'ов - // живёт у них в developer policies, не в ToS proper). Прямой - // формальный запрет в ToS есть ТОЛЬКО у WhatsApp; общая фраза - // «в отличие от других мессенджеров» подчёркивает уникальность - // WhatsApp без неточностей в адрес конкретных сервисов. 'warning.body-1': 'Mautrix-whatsapp подключает ваш аккаунт через тот же механизм связанных устройств, что и WhatsApp Web. Технически это стандартный API — но в отличие от других мессенджеров, условия использования WhatsApp прямо запрещают подключение через сторонние клиенты, и Meta может заблокировать аккаунт за это.', 'warning.body-2': @@ -79,17 +66,12 @@ export const RU = { // src/app/features/bots/BotWidgetEmbed.ts). 'warning.tos-label': 'Условия использования WhatsApp:', 'warning.tos-url': 'https://www.whatsapp.com/legal/terms-of-service', - 'warning.close': 'Понятно', - 'warning.aria-close': 'Закрыть предупреждение', - // Короткий callout в About-модале — пойнтер на отдельную карточку - // с предупреждением. Объяснение «почему» живёт в самой модалке - // warning'а; здесь — только указание куда смотреть, чтобы About - // не дублировал warning по сути. - 'warning.about-callout': - '⚠ Перед подключением WhatsApp прочтите карточку «Прочтите перед подключением» на главном экране бота.', - // --- About panel ------------------------------------------------------- 'card.about.name': 'Как работает WhatsApp-бот', - 'card.about.desc': 'Вход, безопасность и исходный код', + // Hybrid copy: tells the user the modal carries BOTH the «как + // работает» explainer AND the Meta-ToS risk disclosure. «нажмите, + // чтобы прочесть» reinforces interactivity — the amber border + + // warning triangle help but the explicit verb seals it. + 'card.about.desc': 'Информация о работе и рисках — нажмите, чтобы прочесть', 'about.title': 'О боте WhatsApp', 'about.body-1': 'Этот бот подключает WhatsApp к Vojo. После входа личные чаты и группы из WhatsApp появятся в списке чатов Vojo, а ответы из приложения Vojo будут отправляться собеседникам как обычные сообщения в WhatsApp.', diff --git a/apps/widget-whatsapp/src/styles.css b/apps/widget-whatsapp/src/styles.css index 3ae9c48c..591ce0fb 100644 --- a/apps/widget-whatsapp/src/styles.css +++ b/apps/widget-whatsapp/src/styles.css @@ -319,11 +319,14 @@ body { display: block; } -/* Spin the chevron icon while the card is in its `refreshing` in-flight - * state. Combined with `disabled` (which dims the card to opacity 0.5 and - * gates :hover via :not(:disabled)), the spinner is the unambiguous «I'm - * working» signal — no more guessing whether the click registered. */ -.command-card.refreshing .command-card-chevron svg { +/* Spin the leading refresh icon while the card is in its `refreshing` + * in-flight state. Combined with `disabled` (which dims the card to + * opacity 0.5 and gates :hover via :not(:disabled)), the spinner is + * the unambiguous «I'm working» signal — no more guessing whether the + * click registered. The selector targets the leading icon slot since + * the refresh card moved its glyph from the chevron (right) to the + * lead slot (left) for parity with every other card. */ +.command-card.refreshing .command-card-lead-icon svg { animation: command-card-spin 0.8s linear infinite; } @keyframes command-card-spin { @@ -424,45 +427,45 @@ body { border-color: var(--rose); } -/* Warning card — WhatsApp-specific Meta-ToS account-ban risk disclosure. - * Sized like every other card in the grid (auto-fill column on - * desktop, full-width on mobile) — visually distinguished from peers - * by amber tint + border, NOT by spanning the full row. The earlier - * full-width version was visually overweight on a wide desktop grid. - * Amber tint on every layer of chrome (border, name, background) - * without going as loud as `.danger` — this isn't destructive, it's - * «read this before you act». */ +/* Generic leading-icon slot — every command-card carries one as a + * left-side semantic glyph (mirror of the right-side chevron). The + * SVG picks up `currentColor` so it tints with the card's modifier + * — muted by default, amber inside `.command-card.warn`, rose-ish + * inheritance left intentionally OFF for `.danger` (only the title + * goes rose; the lead icon stays muted to keep one accent per card). */ +.command-card-lead-icon { + flex-shrink: 0; + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--muted); +} +.command-card-lead-icon svg { + width: 20px; + height: 20px; + display: block; +} + +/* Warn variant — WhatsApp-only, applied to the AboutCard so the amber + * border, amber-tinted name, and amber lead icon together signal that + * the modal behind this card carries a Meta-ToS risk disclosure (in + * addition to the «how it works» copy). The hybrid card description + * («о работе и рисках») depends on this visual cue to feel honest. */ .command-card.warn { - /* Slight amber tint on the surface itself so the eye can't slide - * past the card on a quick scan; the border underlines it. */ background: rgba(212, 184, 138, 0.06); border-color: var(--amber); } .command-card.warn .command-card-name { color: var(--amber); } +.command-card.warn .command-card-lead-icon { + color: var(--amber); +} :root[data-input='mouse'] .command-card.warn:hover:not(:disabled) { background: rgba(212, 184, 138, 0.12); border-color: var(--amber); } -/* Icon slot to the LEFT of the title block — distinct from the chevron - * which is on the right. The triangle SVG picks up `currentColor` from - * the parent's amber tint so it stays in palette without a separate - * fill rule. */ -.command-card-warn-icon { - flex-shrink: 0; - display: inline-flex; - align-items: center; - justify-content: center; - color: var(--amber); -} -.command-card-warn-icon svg { - width: 22px; - height: 22px; - display: block; -} - /* Inline confirm-in-place body for the destructive logout card. The button * group lives inside the same card frame — no modal, no layout shift. */ .command-card-confirm { @@ -1061,43 +1064,48 @@ body { border-top: 1px solid var(--divider); } -/* WhatsApp-only: Meta-ToS callout that prepends the About body. Same - * tone as `.section-warn-banner` — amber tint + border, distinct - * shape from the body paragraphs around it so it doesn't blend in. +/* WhatsApp-only: Meta-ToS risk-disclosure callout at the top of the + * About modal body. Amber tint + border distinguish it from the plain + * paragraphs that follow — same visual language the dedicated warning + * card used to carry on the disconnected screen, now folded inline. * - * Selector specificity note: `.about-body p { color: var(--text) }` - * (specificity 0,1,1) wins over a bare `.about-warn-callout` rule - * (0,1,0), so the callout's amber `color` would silently lose. Pin - * the callout selector inside `.about-body p.about-warn-callout` - * (0,2,1) to outscore the descendant rule. Background and border - * weren't affected because no competing rule sets them on `.about-body p`. */ -.about-body p.about-warn-callout { - background: rgba(212, 184, 138, 0.10); + * Body text inside the callout still picks up the about-body's + * `.about-body p` rule (default text colour), which is intentional: + * only the title is amber-tinted, the paragraphs stay readable. */ +.about-warn-callout { + background: rgba(212, 184, 138, 0.08); border: 1px solid var(--amber); border-radius: 8px; - padding: 10px 12px; - color: var(--amber); + padding: 12px 14px; + display: flex; + flex-direction: column; + gap: 10px; /* Keep the about-body's `gap: 12px` honoured (no extra margin). */ } -/* Warning modal — same chrome as About modal but with an amber-tinted - * header to make the dialog's purpose visible at a glance. The icon - * sits to the left of the title; the close-X stays on the right. */ -.about-panel-warn { - border-color: var(--amber); +.about-warn-callout-head { + display: flex; + align-items: center; + gap: 10px; } -.about-header-warn { - background: rgba(212, 184, 138, 0.08); -} -.about-header-icon { + +.about-warn-callout-icon { flex-shrink: 0; display: inline-flex; align-items: center; justify-content: center; color: var(--amber); } -.about-header-icon svg { - width: 22px; - height: 22px; +.about-warn-callout-icon svg { + width: 20px; + height: 20px; display: block; } + +.about-warn-callout-title { + font-size: 15px; + font-weight: 600; + color: var(--amber); + margin: 0; + line-height: 1.3; +}