fix(bots-widgets): default data-input to mouse and drop dead danger:hover rose override so hover works from frame zero on hybrid devices

This commit is contained in:
heaven 2026-05-06 17:29:11 +03:00
parent 998813eff4
commit 17ba496b7e
6 changed files with 59 additions and 50 deletions

View file

@ -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) => {

View file

@ -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 {

View file

@ -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) => {

View file

@ -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. */

View file

@ -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) => {

View file

@ -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