/** * Telegram Mini App React Components * Copy-paste into your project * * Requires: hooks.ts from this same skill */ import React, { useState, useCallback, useEffect, ReactNode, CSSProperties } from 'react'; import { createPortal } from 'react-dom'; import { useSafeAreaInset, useFullscreen, useTelegramTheme, getWebApp, isDevMode } from './hooks'; // ============================================================================ // SafeAreaHeader // ============================================================================ interface SafeAreaHeaderProps { children: ReactNode; className?: string; style?: CSSProperties; transparent?: boolean; backgroundColor?: string; position?: 'fixed' | 'sticky'; blur?: boolean; height?: number; border?: boolean; zIndex?: number; } /** * Header with proper safe area handling * * @example * *

My App

*
*/ export function SafeAreaHeader({ children, className = '', style = {}, transparent = false, backgroundColor, position = 'sticky', blur = false, height = 56, border = false, zIndex = 1000, }: SafeAreaHeaderProps) { const { totalTop } = useSafeAreaInset(); const { params, isDark } = useTelegramTheme(); const bgColor = backgroundColor ?? (transparent ? 'transparent' : (params.bg_color ?? '#0f0f1a')); const headerStyle: CSSProperties = { position, top: 0, left: 0, right: 0, zIndex, paddingTop: totalTop, backgroundColor: blur ? `${bgColor}cc` : bgColor, backdropFilter: blur ? 'blur(20px)' : undefined, WebkitBackdropFilter: blur ? 'blur(20px)' : undefined, borderBottom: border ? `1px solid ${isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'}` : undefined, ...style, }; const contentStyle: CSSProperties = { height, display: 'flex', alignItems: 'center', paddingLeft: 16, paddingRight: 16, }; return (
{children}
); } /** * Spacer for fixed SafeAreaHeader */ export function SafeAreaHeaderSpacer({ height = 56 }: { height?: number }) { const { totalTop } = useSafeAreaInset(); return
; } // ============================================================================ // DebugOverlay // ============================================================================ interface DebugOverlayProps { forceShow?: boolean; defaultOpen?: boolean; position?: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; } /** * Debug panel showing safe areas, viewport, platform info * Only visible on localhost or with ?debug=1 * * @example * // Add to app root */ export function DebugOverlay({ forceShow = false, defaultOpen = false, position = 'bottom-right', }: DebugOverlayProps) { const [isOpen, setIsOpen] = useState(defaultOpen); const [copiedKey, setCopiedKey] = useState(null); const insets = useSafeAreaInset(); const { isFullscreen } = useFullscreen(); if (!forceShow && !isDevMode()) return null; const webApp = getWebApp(); const values = [ { label: 'Platform', value: webApp?.platform ?? 'unknown' }, { label: 'Version', value: webApp?.version ?? 'N/A' }, { label: 'Fullscreen', value: isFullscreen ? '✓' : '✗' }, { label: 'Viewport', value: `${window.innerWidth}×${webApp?.viewportHeight ?? window.innerHeight}` }, { label: 'Safe Top', value: insets.device.top }, { label: 'Safe Bottom', value: insets.device.bottom }, { label: 'Content Top', value: insets.content.top }, { label: 'Content Bottom', value: insets.content.bottom }, { label: '⚡ Total Top', value: insets.totalTop }, { label: '⚡ Total Bottom', value: insets.totalBottom }, ]; const handleCopy = async (label: string, value: any) => { await navigator.clipboard.writeText(String(value)); setCopiedKey(label); setTimeout(() => setCopiedKey(null), 1000); }; const positionStyle: CSSProperties = { 'top-left': { top: 8, left: 8 }, 'top-right': { top: 8, right: 8 }, 'bottom-left': { bottom: 8, left: 8 }, 'bottom-right': { bottom: 8, right: 8 }, }[position]; return createPortal( <> {isOpen && (
TG DEBUG
{values.map(({ label, value }) => (
handleCopy(label, value)} style={{ display: 'flex', justifyContent: 'space-between', padding: '4px 6px', cursor: 'pointer', backgroundColor: copiedKey === label ? 'rgba(34,197,94,0.3)' : 'transparent', borderRadius: 4, }} > {label} {value}
))}
Tap to copy
)} , document.body ); } // ============================================================================ // Modal (uses createPortal to fix position:fixed issue) // ============================================================================ interface ModalProps { children: ReactNode; isOpen: boolean; onClose: () => void; zIndex?: number; } /** * Modal that works in Telegram Mini Apps * Uses createPortal to avoid position:fixed issues * * @example * setShow(false)}> *
Modal content
*
*/ export function Modal({ children, isOpen, onClose, zIndex = 9999 }: ModalProps) { const { totalBottom } = useSafeAreaInset(); useEffect(() => { if (isOpen) { document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = ''; }; } }, [isOpen]); if (!isOpen) return null; return createPortal(
{ if (e.target === e.currentTarget) onClose(); }} > {children}
, document.body ); }