77 lines
3.2 KiB
TypeScript
77 lines
3.2 KiB
TypeScript
import { render } from 'preact';
|
|
import { readBootstrap } from './bootstrap';
|
|
import { App } from './App';
|
|
import { createT } from './i18n';
|
|
import { WidgetApi } from './widget-api';
|
|
import './styles.css';
|
|
|
|
// 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»).
|
|
//
|
|
// 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 and
|
|
// dropped: every interaction-media query is mis-reported on at least one
|
|
// shipping device (see git history for the survey). Defaulting to 'mouse'
|
|
// is strictly no worse on any device: a desktop user gets hover from frame
|
|
// zero, and a touch user cannot trigger `:hover` before tapping — by the
|
|
// time the first tap fires, our listener has already moved the attribute
|
|
// to 'touch'.
|
|
const setInputMode = (mode: 'touch' | 'mouse'): void => {
|
|
document.documentElement.dataset.input = mode;
|
|
};
|
|
setInputMode('mouse');
|
|
window.addEventListener(
|
|
'pointerdown',
|
|
(event) => {
|
|
setInputMode(event.pointerType === 'mouse' ? 'mouse' : 'touch');
|
|
},
|
|
{ passive: true, capture: true }
|
|
);
|
|
|
|
const root = document.getElementById('app');
|
|
if (!root) {
|
|
throw new Error('#app root element missing — index.html out of sync');
|
|
}
|
|
|
|
const result = readBootstrap(window.location.search);
|
|
|
|
if (!result.ok) {
|
|
// Either someone opened the widget URL directly (no host params), or a
|
|
// host bug failed to provide them. Either way render a self-contained
|
|
// diagnostic instead of going silent. Bootstrap failed before we could
|
|
// read clientLanguage from the URL, so let createT fall back to
|
|
// navigator.language.
|
|
const t = createT();
|
|
render(
|
|
<div class="app">
|
|
<div class="error-banner">
|
|
<strong>{t('bootstrap.failed')}</strong>
|
|
{t('bootstrap.missing-params', { names: result.missing.join(', ') })}{' '}
|
|
{t('bootstrap.embedded-only', { route: '/bots/telegram' })}
|
|
</div>
|
|
</div>,
|
|
root
|
|
);
|
|
} else {
|
|
// Apply initial theme synchronously so the first paint isn't flashed
|
|
// through the wrong palette.
|
|
document.documentElement.dataset.theme = result.bootstrap.theme;
|
|
|
|
// Instantiate the WidgetApi BEFORE the first render. The constructor
|
|
// attaches the `window.addEventListener('message', ...)` listener
|
|
// synchronously, so by the time the host's ClientWidgetApi fires its
|
|
// capabilities request on iframe `load` we're already listening. On a
|
|
// cached-bundle remount the request can race ahead of any useEffect —
|
|
// construction at module-load closes that window.
|
|
const api = new WidgetApi(result.bootstrap);
|
|
render(<App bootstrap={result.bootstrap} api={api} />, root);
|
|
}
|