diff --git a/apps/widget-discord/src/main.tsx b/apps/widget-discord/src/main.tsx index 4324950f..40e2ff54 100644 --- a/apps/widget-discord/src/main.tsx +++ b/apps/widget-discord/src/main.tsx @@ -6,16 +6,15 @@ import { WidgetApi, buildCapabilities } from './widget-api'; import './styles.css'; // Input-mode detector — see apps/widget-telegram/src/main.tsx for the -// full rationale. Capacitor Android WebView mis-reports `hover: hover` -// on touch devices, so we drive `:hover` styling off the actual -// `pointerdown.pointerType` instead of media queries. The initial guess -// based on `(any-pointer: coarse)` covers the pre-first-pointerdown -// frame so a touch device doesn't briefly show mouse-mode hover -// affordances if the user immediately taps a card. +// full rationale. Default to 'mouse'; the capture-phase pointerdown +// listener flips to 'touch' on the first non-mouse pointerType. +// matchMedia guessing was dropped — every variant +// (`any-pointer: coarse|fine`, `hover: hover`, `pointer: fine|coarse`) +// is mis-reported on at least one shipping device. const setInputMode = (mode: 'touch' | 'mouse'): void => { document.documentElement.dataset.input = mode; }; -setInputMode(window.matchMedia('(any-pointer: coarse)').matches ? 'touch' : 'mouse'); +setInputMode('mouse'); window.addEventListener( 'pointerdown', (event) => { diff --git a/apps/widget-discord/src/styles.css b/apps/widget-discord/src/styles.css index 4423ff50..c3e4dd73 100644 --- a/apps/widget-discord/src/styles.css +++ b/apps/widget-discord/src/styles.css @@ -376,13 +376,16 @@ body { } /* Destructive card — red name marks logout as destructive vs the primary - * login card. */ + * login card. The hover border stays on the generic + * `:root[data-input='mouse'] .command-card:hover` rule (hairline) so the + * accent is consistent across touch and mouse modes — a previous + * `.command-card.danger:hover { border-color: var(--rose) }` override + * was dead in mouse mode (lower specificity than the input-gated rule) + * and only fired in the pre-first-pointerdown touch stub, leaking a + * rose tint that looked amber on the dark surface. */ .command-card.danger .command-card-name { color: var(--rose); } -.command-card.danger:hover:not(:disabled) { - border-color: var(--rose); -} /* Inline confirm-in-place body for the destructive logout card. */ .command-card-confirm { diff --git a/apps/widget-telegram/src/main.tsx b/apps/widget-telegram/src/main.tsx index 953bf766..5e208b6a 100644 --- a/apps/widget-telegram/src/main.tsx +++ b/apps/widget-telegram/src/main.tsx @@ -5,25 +5,37 @@ import { createT } from './i18n'; import { WidgetApi, buildCapabilities } from './widget-api'; import './styles.css'; -// Input-mode detector. Capacitor's Android Chromium WebView reports -// `(hover: hover)` and `(any-pointer: fine)` as TRUE on a pure-touch -// device — verified via on-device console.log of `matchMedia(...).matches`. -// That makes media-query gating of `:hover` styles unreliable: the rule -// fires on touch and then sticks (Chromium WebView synthesises `:hover` on -// the focused element after a tap and never clears it until the next -// interaction elsewhere). The visible symptom is a card that «greys out -// after tap and only un-greys when you tap a different button». +// Input-mode detector for hover styling. CSS gates `:hover` and +// `:focus-visible` rules on `:root[data-input="mouse"]` because Capacitor's +// Android Chromium WebView synthesises `:hover` on the focused element +// after a tap and never clears it until the next interaction elsewhere — +// without the gate, every tap leaves a sticky hover state on the tapped +// card («card greys out after tap and only un-greys when you tap a +// different button»). // -// Real input is determined from the actual `pointerdown.pointerType` at -// runtime. The first pointerdown after load is authoritative; CSS gates -// hover styling via `:root[data-input="mouse"]`. The initial guess based -// on `(any-pointer: coarse)` covers the pre-first-pointerdown frame so -// the first paint on a touch device doesn't briefly show the mouse-mode -// hover affordances if the user immediately taps a card. +// Truth comes from `pointerdown.pointerType`. The capture-phase listener +// runs in the same task as any post-tap `:hover` synthesis, so a touch +// tap on Android lands in 'touch' mode in the same render frame as the +// synthesised hover would paint. +// +// Initial mode is plain 'mouse'. matchMedia-based guessing was tried +// here and dropped — every interaction-media query is mis-reported on at +// least one shipping device: Capacitor Android WebView falsely matches +// `hover: hover` and `any-pointer: fine` on pure-touch phones; +// Samsung / OnePlus / Moto Androids expose a virtual-mouse HID and +// falsely match `pointer: fine`; older Firefox-on-Windows desktops +// reported `pointer: coarse` despite a real mouse. Defaulting to 'mouse' +// is strictly no worse than any of those queries on any device: a +// desktop / hybrid user gets hover affordances from frame zero, and a +// touch user cannot trigger `:hover` before tapping because there is no +// pointer hovering anything — by the time the first tap fires +// `:hover` (synthesised), our listener has already moved the attribute +// to 'touch'. Pen / stylus also lands in 'touch' (pointerType is `pen`, +// matched by the `!== 'mouse'` branch). const setInputMode = (mode: 'touch' | 'mouse'): void => { document.documentElement.dataset.input = mode; }; -setInputMode(window.matchMedia('(any-pointer: coarse)').matches ? 'touch' : 'mouse'); +setInputMode('mouse'); window.addEventListener( 'pointerdown', (event) => { diff --git a/apps/widget-telegram/src/styles.css b/apps/widget-telegram/src/styles.css index 22541293..111497f5 100644 --- a/apps/widget-telegram/src/styles.css +++ b/apps/widget-telegram/src/styles.css @@ -438,13 +438,15 @@ body { } /* Destructive card — keeps the red name to mark «Выйти из Telegram» as a - * destructive action, distinguishing it from the primary login card. */ + * destructive action, distinguishing it from the primary login card. + * Hover border stays on the generic input-gated rule (hairline) so the + * accent is consistent across touch and mouse modes. A previous + * `.command-card.danger:hover { border-color: var(--rose) }` override + * was dead in mouse mode (lower specificity than the input-gated rule) + * and only fired in the pre-first-pointerdown touch stub. */ .command-card.danger .command-card-name { color: var(--rose); } -.command-card.danger:hover:not(:disabled) { - border-color: var(--rose); -} /* Inline confirm-in-place body for the destructive logout card. The button * group lives inside the same card frame — no modal, no layout shift. */ diff --git a/apps/widget-whatsapp/src/main.tsx b/apps/widget-whatsapp/src/main.tsx index 5fc5d57f..83034990 100644 --- a/apps/widget-whatsapp/src/main.tsx +++ b/apps/widget-whatsapp/src/main.tsx @@ -5,25 +5,16 @@ import { createT } from './i18n'; import { WidgetApi, buildCapabilities } from './widget-api'; import './styles.css'; -// Input-mode detector. Capacitor's Android Chromium WebView reports -// `(hover: hover)` and `(any-pointer: fine)` as TRUE on a pure-touch -// device — verified via on-device console.log of `matchMedia(...).matches`. -// That makes media-query gating of `:hover` styles unreliable: the rule -// fires on touch and then sticks (Chromium WebView synthesises `:hover` on -// the focused element after a tap and never clears it until the next -// interaction elsewhere). The visible symptom is a card that «greys out -// after tap and only un-greys when you tap a different button». -// -// Real input is determined from the actual `pointerdown.pointerType` at -// runtime. The first pointerdown after load is authoritative; CSS gates -// hover styling via `:root[data-input="mouse"]`. The initial guess based -// on `(any-pointer: coarse)` covers the pre-first-pointerdown frame so -// the first paint on a touch device doesn't briefly show the mouse-mode -// hover affordances if the user immediately taps a card. +// Input-mode detector — see apps/widget-telegram/src/main.tsx for the +// full rationale. Default to 'mouse'; the capture-phase pointerdown +// listener flips to 'touch' on the first non-mouse pointerType. +// matchMedia guessing was dropped — every variant +// (`any-pointer: coarse|fine`, `hover: hover`, `pointer: fine|coarse`) +// is mis-reported on at least one shipping device. const setInputMode = (mode: 'touch' | 'mouse'): void => { document.documentElement.dataset.input = mode; }; -setInputMode(window.matchMedia('(any-pointer: coarse)').matches ? 'touch' : 'mouse'); +setInputMode('mouse'); window.addEventListener( 'pointerdown', (event) => { diff --git a/apps/widget-whatsapp/src/styles.css b/apps/widget-whatsapp/src/styles.css index d7f1685f..d5d54b86 100644 --- a/apps/widget-whatsapp/src/styles.css +++ b/apps/widget-whatsapp/src/styles.css @@ -419,13 +419,15 @@ body { } /* Destructive card — keeps the red name to mark «Выйти из WhatsApp» as a - * destructive action, distinguishing it from the primary login card. */ + * destructive action, distinguishing it from the primary login card. + * Hover border stays on the generic input-gated rule (hairline) so the + * accent is consistent across touch and mouse modes. A previous + * `.command-card.danger:hover { border-color: var(--rose) }` override + * was dead in mouse mode (lower specificity than the input-gated rule) + * and only fired in the pre-first-pointerdown touch stub. */ .command-card.danger .command-card-name { color: var(--rose); } -.command-card.danger:hover:not(:disabled) { - border-color: var(--rose); -} /* Generic leading-icon slot — every command-card carries one as a * left-side semantic glyph (mirror of the right-side chevron). The