Merge pull request 'fix(ui): mobile WebView polish — tap-flash, VK haptics, VK swipe-back' (#149) from feature/mobile-webview-polish into development
This commit was merged in pull request #149.
This commit is contained in:
+9
-1
@@ -100,7 +100,10 @@ e2e and the screenshots.
|
||||
square with an accent underline). A red count **badge** rides the icon's corner — on the
|
||||
lobby ⚙️ tab and the hub's 🤝 Friends tab for pending incoming friend requests (invitations
|
||||
keep their own lobby section), and on the Hint tab for the remaining count. No text
|
||||
selection on nav / tab-bar / buttons (`user-select: none`).
|
||||
selection on nav / tab-bar / buttons (`user-select: none`), and no tap-highlight flash on any
|
||||
tappable element (`-webkit-tap-highlight-color: transparent` on `#app`) — Android in-app WebViews
|
||||
otherwise draw a momentary selection-like box on a clickable node on tap (seen on the header title
|
||||
and the back chevron).
|
||||
- **Screen transitions** (`App.svelte`): navigation slides directionally — a
|
||||
screen entered from the lobby flies in from the right; returning to the lobby reveals it
|
||||
from the left (back). Transitions are local (so they do not play on first load) and
|
||||
@@ -146,6 +149,11 @@ e2e and the screenshots.
|
||||
insets (`VKWebAppUpdateInsets`, needed on Android, where the VK webview exposes no `env()` inset),
|
||||
so the layout clears the VK home bar. Share and copy route through the bridge (`VKWebAppShare` /
|
||||
`VKWebAppCopyText`) because `navigator.share` / `clipboard` are absent in the desktop VK iframe.
|
||||
Haptics fire the same set as Telegram via VK Bridge taptic (`VKWebAppTapticImpactOccurred` /
|
||||
`…NotificationOccurred` / `…SelectionChanged`), through the shared `lib/haptics.ts` dispatcher; and
|
||||
VK's **horizontal swipe-back** is disabled at launch (`VKWebAppSetSwipeSettings`) so it does not
|
||||
fight the app's own edge-swipe-back and tile drag — parity with Telegram's disabled vertical
|
||||
swipes, the app owning navigation through its back chevron.
|
||||
|
||||
## Tiles & board
|
||||
|
||||
|
||||
@@ -171,6 +171,10 @@ html.app-shell body {
|
||||
/* No text selection anywhere by default; inputs opt back in below. */
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
/* No tap-highlight flash on tappable elements. Android in-app WebViews (Telegram, VK) draw a
|
||||
momentary selection-like box on a clickable node on tap — seen on the header title and the
|
||||
back chevron; user-select does not govern it. Inherited, so this clears it app-wide. */
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
input,
|
||||
|
||||
@@ -124,6 +124,17 @@
|
||||
.back:hover {
|
||||
background: var(--surface-2);
|
||||
}
|
||||
/* Android in-app WebViews (Telegram, VK) flash a selection-like box on the two tappable header
|
||||
controls — the title (a 10-tap debug target) and the back chevron — on a plain tap. The global
|
||||
-webkit-user-select / -webkit-tap-highlight-color on #app is inherited, but those WebViews only
|
||||
suppress the artifact when the properties sit DIRECTLY on the tapped element. */
|
||||
h1,
|
||||
.back {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
/* A thin, compact "<" drawn from two borders — lighter than a glyph. */
|
||||
.chev {
|
||||
width: 11px;
|
||||
|
||||
@@ -24,7 +24,8 @@
|
||||
import { getCachedGame, setCachedGame, setCachedDraft, type CachedGame } from '../lib/gamecache';
|
||||
import { patchLobbyGame } from '../lib/lobbycache';
|
||||
import { applyGameOver, applyMoveDelta, applyOpponentJoined, type DeltaResult } from '../lib/gamedelta';
|
||||
import { insideTelegram, telegramDialogsAvailable, telegramHaptic, telegramShowConfirm } from '../lib/telegram';
|
||||
import { insideTelegram, telegramDialogsAvailable, telegramShowConfirm } from '../lib/telegram';
|
||||
import { haptic } from '../lib/haptics';
|
||||
import {
|
||||
BLANK,
|
||||
newPlacement,
|
||||
@@ -511,7 +512,7 @@
|
||||
if (drag && isCoarse() && !zoomed) {
|
||||
focus = c;
|
||||
zoomed = true;
|
||||
telegramHaptic('light');
|
||||
haptic('light');
|
||||
}
|
||||
}, 700)
|
||||
: null;
|
||||
@@ -623,7 +624,7 @@
|
||||
return;
|
||||
}
|
||||
placement = place(placement, index, row, col);
|
||||
telegramHaptic('select');
|
||||
haptic('select');
|
||||
recompute();
|
||||
scheduleDraftSave();
|
||||
}
|
||||
@@ -631,7 +632,7 @@
|
||||
if (!blankPrompt) return;
|
||||
placement = place(placement, blankPrompt.rackIndex, blankPrompt.row, blankPrompt.col, letter);
|
||||
blankPrompt = null;
|
||||
telegramHaptic('select');
|
||||
haptic('select');
|
||||
recompute();
|
||||
scheduleDraftSave();
|
||||
}
|
||||
@@ -687,7 +688,7 @@
|
||||
busy = true;
|
||||
try {
|
||||
applyMoveResult(await gateway.submitPlay(id, sub.tiles, variant));
|
||||
telegramHaptic('success');
|
||||
haptic('success');
|
||||
zoomed = false;
|
||||
} catch (e) {
|
||||
handleError(e);
|
||||
@@ -793,7 +794,7 @@
|
||||
shuffling = true;
|
||||
setTimeout(() => (shuffling = false), 600);
|
||||
// A short "shake": a few quick light taps rather than one.
|
||||
for (let i = 0; i < 4; i++) setTimeout(() => telegramHaptic('light'), i * 55);
|
||||
for (let i = 0; i < 4; i++) setTimeout(() => haptic('light'), i * 55);
|
||||
scheduleDraftSave();
|
||||
}
|
||||
function openExchange() {
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
telegramSafeAreaInset,
|
||||
telegramDisableVerticalSwipes,
|
||||
telegramShowSettingsButton,
|
||||
telegramHaptic,
|
||||
telegramLaunch,
|
||||
type TelegramLaunch,
|
||||
telegramOnEvent,
|
||||
@@ -32,7 +31,8 @@ import {
|
||||
telegramCloudGet,
|
||||
telegramCloudSet,
|
||||
} from './telegram';
|
||||
import { onVKPath, insideVK, vkInit, vkLaunchParams, vkUserName, vkStartParam, vkOnScheme, vkOnInsets } from './vk';
|
||||
import { onVKPath, insideVK, vkInit, vkDisableSwipeBack, vkLaunchParams, vkUserName, vkStartParam, vkOnScheme, vkOnInsets } from './vk';
|
||||
import { haptic } from './haptics';
|
||||
import { CLOUD_PREFS_KEY, decodeClientPrefs, encodeClientPrefs } from './cloudprefs';
|
||||
import { parseStartParam } from './deeplink';
|
||||
import {
|
||||
@@ -281,7 +281,7 @@ export function handleError(err: unknown): void {
|
||||
return;
|
||||
}
|
||||
if (isConnectionCode(code) || !connection.online) return;
|
||||
telegramHaptic('error');
|
||||
haptic('error');
|
||||
showToast(t(code ? errorKey(code) : 'error.generic'), 'error');
|
||||
}
|
||||
|
||||
@@ -705,6 +705,9 @@ export async function bootstrap(): Promise<void> {
|
||||
root.style.setProperty('--tg-safe-right', `max(env(safe-area-inset-right, 0px), ${i.right}px)`);
|
||||
});
|
||||
await vkInit();
|
||||
// The app owns navigation (its own back chevron), so silence VK's horizontal swipe-back to keep
|
||||
// it off our edge-swipe-back and on-board tile drag — parity with telegramDisableVerticalSwipes.
|
||||
void vkDisableSwipeBack();
|
||||
await bootVK();
|
||||
app.ready = true;
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// Cross-platform haptic feedback. The app fires one Haptic vocabulary from its interaction code
|
||||
// (tile placement, submit, errors); this routes it to whichever host Mini App is in play —
|
||||
// Telegram's HapticFeedback or VK Bridge's taptic events — and stays silent in a plain browser.
|
||||
|
||||
import { type Haptic, telegramHaptic } from './telegram';
|
||||
import { insideVK, vkHaptic } from './vk';
|
||||
|
||||
/**
|
||||
* haptic fires the host Mini App's haptic feedback for kind: Telegram's HapticFeedback inside
|
||||
* Telegram and VK Bridge's taptic inside VK; a no-op in an ordinary browser. Both backends
|
||||
* self-guard off their platform, so this is always safe to call. The VK send is fire-and-forget.
|
||||
*/
|
||||
export function haptic(kind: Haptic): void {
|
||||
telegramHaptic(kind);
|
||||
if (insideVK()) void vkHaptic(kind);
|
||||
}
|
||||
@@ -6,6 +6,8 @@
|
||||
// (VKWebAppGetUserInfo, since VK omits it from the signed launch params). Every helper is safe to
|
||||
// call outside VK.
|
||||
|
||||
import type { Haptic } from './telegram';
|
||||
|
||||
async function bridge() {
|
||||
return (await import('@vkontakte/vk-bridge')).default;
|
||||
}
|
||||
@@ -160,3 +162,39 @@ export async function vkOnInsets(handler: (insets: VKInsets) => void): Promise<v
|
||||
// Outside VK / bridge unavailable: the CSS env() safe-area fallback applies.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* vkHaptic fires a VK Bridge haptic mirroring the Telegram set: a selection tick
|
||||
* (VKWebAppTapticSelectionChanged), a success/warning/error notification
|
||||
* (VKWebAppTapticNotificationOccurred) or a light/medium/heavy impact (VKWebAppTapticImpactOccurred).
|
||||
* Best-effort and a no-op outside VK or on a client without taptic support (the send rejects and is
|
||||
* swallowed), so callers fire it unconditionally.
|
||||
*/
|
||||
export async function vkHaptic(kind: Haptic): Promise<void> {
|
||||
try {
|
||||
const b = await bridge();
|
||||
if (kind === 'select') {
|
||||
await b.send('VKWebAppTapticSelectionChanged', {});
|
||||
} else if (kind === 'success' || kind === 'warning' || kind === 'error') {
|
||||
await b.send('VKWebAppTapticNotificationOccurred', { type: kind });
|
||||
} else {
|
||||
await b.send('VKWebAppTapticImpactOccurred', { style: kind });
|
||||
}
|
||||
} catch {
|
||||
// Outside VK / no taptic support: silent, like the Telegram haptic off-platform.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* vkDisableSwipeBack turns off VK's horizontal swipe-back gesture (VKWebAppSetSwipeSettings with
|
||||
* history:false) so it does not fight the app's own edge-swipe-back and on-board tile drag — the
|
||||
* app owns navigation through its back chevron, as on Telegram (telegramDisableVerticalSwipes).
|
||||
* Best-effort; a no-op outside VK or on a client without the method.
|
||||
*/
|
||||
export async function vkDisableSwipeBack(): Promise<void> {
|
||||
try {
|
||||
await (await bridge()).send('VKWebAppSetSwipeSettings', { history: false });
|
||||
} catch {
|
||||
// Outside VK / unsupported: VK's default gesture handling stays as-is.
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user