feat: enhance CSS and components for Telegram Mini App performance
- Updated CSS to utilize viewport variables for safe area insets and stable height, improving layout consistency across devices. - Introduced haptic feedback triggers in various components to enhance user interaction, mimicking native Telegram behavior. - Added functionality to detect Android performance class, minimizing animations on low-performance devices for better user experience. - Refactored components to incorporate new CSS classes for content safety and improved responsiveness.
This commit is contained in:
@@ -17,6 +17,7 @@ import { CalendarGrid } from "@/components/calendar/CalendarGrid";
|
||||
import { DutyList } from "@/components/duty/DutyList";
|
||||
import { DayDetail, type DayDetailHandle } from "@/components/day-detail";
|
||||
import { ErrorState } from "@/components/states/ErrorState";
|
||||
import { triggerHapticLight } from "@/lib/telegram-haptic";
|
||||
|
||||
/** Fallback height (px) until ResizeObserver reports the sticky block size. Matches --calendar-block-min-height + pb-2 (260 + 8). */
|
||||
const STICKY_HEIGHT_FALLBACK_PX = 268;
|
||||
@@ -90,10 +91,12 @@ export function CalendarPage({ isAllowed, initDataRaw }: CalendarPageProps) {
|
||||
const navDisabled = loading || accessDenied || selectedDay !== null;
|
||||
const handlePrevMonth = useCallback(() => {
|
||||
if (navDisabled) return;
|
||||
triggerHapticLight();
|
||||
prevMonth();
|
||||
}, [navDisabled, prevMonth]);
|
||||
const handleNextMonth = useCallback(() => {
|
||||
if (navDisabled) return;
|
||||
triggerHapticLight();
|
||||
nextMonth();
|
||||
}, [navDisabled, nextMonth]);
|
||||
|
||||
@@ -133,7 +136,7 @@ export function CalendarPage({ isAllowed, initDataRaw }: CalendarPageProps) {
|
||||
}, [loading, accessDenied, setAppContentReady]);
|
||||
|
||||
return (
|
||||
<div className="mx-auto flex min-h-screen w-full max-w-[var(--max-width-app)] flex-col bg-background px-3 pb-6 pt-safe">
|
||||
<div className="content-safe mx-auto flex min-h-screen w-full max-w-[var(--max-width-app)] flex-col bg-background px-3 pb-6">
|
||||
<div
|
||||
ref={calendarStickyRef}
|
||||
className="sticky top-0 z-10 min-h-[var(--calendar-block-min-height)] bg-background pb-2"
|
||||
|
||||
@@ -9,6 +9,7 @@ import React, { useMemo } from "react";
|
||||
import { useTranslation } from "@/i18n/use-translation";
|
||||
import { dateKeyToDDMM } from "@/lib/date-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { triggerHapticLight } from "@/lib/telegram-haptic";
|
||||
import type { DutyWithUser } from "@/types";
|
||||
import { DayIndicators } from "./DayIndicators";
|
||||
|
||||
@@ -89,6 +90,7 @@ function CalendarDayInner({
|
||||
)}
|
||||
onClick={(e) => {
|
||||
if (isOtherMonth) return;
|
||||
triggerHapticLight();
|
||||
onDayClick(dateKey, e.currentTarget.getBoundingClientRect());
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
formatHHMM,
|
||||
} from "@/lib/date-utils";
|
||||
import { getRemainingTime, findCurrentDuty } from "@/lib/current-duty";
|
||||
import { triggerHapticLight } from "@/lib/telegram-haptic";
|
||||
import { ContactLinks } from "@/components/contact/ContactLinks";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -145,10 +146,12 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
|
||||
}, [onBack]);
|
||||
|
||||
const handleBack = () => {
|
||||
triggerHapticLight();
|
||||
onBack();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
triggerHapticLight();
|
||||
if (closeMiniApp.isAvailable()) {
|
||||
closeMiniApp();
|
||||
} else {
|
||||
@@ -213,6 +216,7 @@ export function CurrentDutyView({ onBack, openedFromPin = false }: CurrentDutyVi
|
||||
|
||||
if (state === "error") {
|
||||
const handleRetry = () => {
|
||||
triggerHapticLight();
|
||||
setState("loading");
|
||||
loadTodayDuties();
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ import { DayDetailContent } from "./DayDetailContent";
|
||||
import type { CalendarEvent, DutyWithUser } from "@/types";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { triggerHapticLight } from "@/lib/telegram-haptic";
|
||||
|
||||
/** Empty state for day detail: date and "no duties or events" message. */
|
||||
function DayDetailEmpty({ dateKey }: { dateKey: string }) {
|
||||
@@ -120,6 +121,7 @@ export const DayDetail = React.forwardRef<DayDetailHandle, DayDetailProps>(
|
||||
|
||||
/** Start close animation; actual unmount happens in onCloseAnimationEnd (or fallback timeout). */
|
||||
const requestClose = React.useCallback(() => {
|
||||
triggerHapticLight();
|
||||
setExiting(true);
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
formatHHMM,
|
||||
} from "@/lib/date-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { triggerHapticLight } from "@/lib/telegram-haptic";
|
||||
import { ContactLinks } from "@/components/contact/ContactLinks";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import type { DutyWithUser } from "@/types";
|
||||
@@ -77,6 +78,7 @@ export function DutyTimelineCard({
|
||||
const flipped = isControlled ? (isFlipped ?? false) : localFlipped;
|
||||
|
||||
const handleFlipToBack = () => {
|
||||
triggerHapticLight();
|
||||
if (isControlled) {
|
||||
onFlipChange?.(true);
|
||||
} else {
|
||||
@@ -86,6 +88,7 @@ export function DutyTimelineCard({
|
||||
};
|
||||
|
||||
const handleFlipToFront = () => {
|
||||
triggerHapticLight();
|
||||
if (isControlled) {
|
||||
onFlipChange?.(false);
|
||||
} else {
|
||||
|
||||
@@ -6,15 +6,20 @@ import {
|
||||
mountMiniAppSync,
|
||||
mountThemeParamsSync,
|
||||
bindThemeParamsCssVars,
|
||||
mountViewport,
|
||||
bindViewportCssVars,
|
||||
unmountViewport,
|
||||
} from "@telegram-apps/sdk-react";
|
||||
import { fixSurfaceContrast } from "@/hooks/use-telegram-theme";
|
||||
import { applyAndroidPerformanceClass } from "@/lib/telegram-android-perf";
|
||||
|
||||
/**
|
||||
* Wraps the app with Telegram Mini App SDK initialization.
|
||||
* Calls init(acceptCustomStyles), mounts theme params (binds --tg-theme-* CSS vars),
|
||||
* and mounts the mini app. Does not call ready() here — the app calls
|
||||
* callMiniAppReadyOnce() from lib/telegram-ready when the first visible screen
|
||||
* has finished loading, so Telegram keeps its native loading animation until then.
|
||||
* mounts the mini app, then mounts viewport and binds viewport CSS vars
|
||||
* (--tg-viewport-stable-height, --tg-viewport-content-safe-area-inset-*, etc.).
|
||||
* Does not call ready() here — the app calls callMiniAppReadyOnce() from
|
||||
* lib/telegram-ready when the first visible screen has finished loading.
|
||||
* Theme is set before first paint by the inline script in layout.tsx (URL hash);
|
||||
* useTelegramTheme() in the app handles ongoing theme changes.
|
||||
*/
|
||||
@@ -39,7 +44,26 @@ export function TelegramProvider({
|
||||
mountMiniAppSync();
|
||||
}
|
||||
|
||||
return cleanup;
|
||||
applyAndroidPerformanceClass();
|
||||
|
||||
let unbindViewportCssVars: (() => void) | undefined;
|
||||
if (mountViewport.isAvailable()) {
|
||||
mountViewport()
|
||||
.then(() => {
|
||||
if (bindViewportCssVars.isAvailable()) {
|
||||
unbindViewportCssVars = bindViewportCssVars();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
// Viewport not supported (e.g. not in Mini App); ignore.
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
unbindViewportCssVars?.();
|
||||
unmountViewport();
|
||||
cleanup();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <>{children}</>;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { useEffect } from "react";
|
||||
import { getLang, translate } from "@/i18n/messages";
|
||||
import { useAppStore } from "@/store/app-store";
|
||||
import { triggerHapticLight } from "@/lib/telegram-haptic";
|
||||
|
||||
export interface AccessDeniedScreenProps {
|
||||
/** Optional detail from API 403 response, shown below the hint. */
|
||||
@@ -40,6 +41,7 @@ export function AccessDeniedScreen({
|
||||
const hasDetail = Boolean(serverDetail && String(serverDetail).trim());
|
||||
|
||||
const handleClick = () => {
|
||||
triggerHapticLight();
|
||||
if (primaryAction === "reload") {
|
||||
if (typeof window !== "undefined") {
|
||||
window.location.reload();
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import { useTranslation } from "@/i18n/use-translation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { triggerHapticLight } from "@/lib/telegram-haptic";
|
||||
|
||||
export interface ErrorStateProps {
|
||||
/** Error message to display. If not provided, uses generic i18n message. */
|
||||
@@ -65,7 +66,10 @@ export function ErrorState({ message, onRetry, className }: ErrorStateProps) {
|
||||
variant="default"
|
||||
size="sm"
|
||||
className="mt-1 bg-primary text-primary-foreground hover:opacity-90 focus-visible:outline-2 focus-visible:outline-accent focus-visible:outline-offset-2"
|
||||
onClick={onRetry}
|
||||
onClick={() => {
|
||||
triggerHapticLight();
|
||||
onRetry();
|
||||
}}
|
||||
>
|
||||
{t("error.retry")}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user