mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useRef, useEffect } from 'react'
|
import { useState, useRef, useEffect, useMemo, useCallback } from 'react'
|
||||||
|
|
||||||
interface ColorPickerProps {
|
interface ColorPickerProps {
|
||||||
value: string
|
value: string
|
||||||
@@ -8,6 +8,28 @@ interface ColorPickerProps {
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if running in Telegram WebApp (native color picker causes crash)
|
||||||
|
const isTelegramWebApp = (): boolean => {
|
||||||
|
return !!(window as unknown as { Telegram?: { WebApp?: unknown } }).Telegram?.WebApp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert hex to RGB
|
||||||
|
const hexToRgb = (hex: string): { r: number; g: number; b: number } => {
|
||||||
|
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex)
|
||||||
|
return result
|
||||||
|
? {
|
||||||
|
r: parseInt(result[1], 16),
|
||||||
|
g: parseInt(result[2], 16),
|
||||||
|
b: parseInt(result[3], 16),
|
||||||
|
}
|
||||||
|
: { r: 0, g: 0, b: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert RGB to hex
|
||||||
|
const rgbToHex = (r: number, g: number, b: number): string => {
|
||||||
|
return '#' + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')
|
||||||
|
}
|
||||||
|
|
||||||
const PRESET_COLORS = [
|
const PRESET_COLORS = [
|
||||||
'#3b82f6', // Blue
|
'#3b82f6', // Blue
|
||||||
'#ef4444', // Red
|
'#ef4444', // Red
|
||||||
@@ -21,18 +43,36 @@ const PRESET_COLORS = [
|
|||||||
'#f97316', // Orange
|
'#f97316', // Orange
|
||||||
'#6366f1', // Indigo
|
'#6366f1', // Indigo
|
||||||
'#a855f7', // Purple
|
'#a855f7', // Purple
|
||||||
|
'#ffffff', // White
|
||||||
|
'#64748b', // Slate
|
||||||
|
'#1e293b', // Dark
|
||||||
|
'#000000', // Black
|
||||||
]
|
]
|
||||||
|
|
||||||
export function ColorPicker({ value, onChange, label, description, disabled }: ColorPickerProps) {
|
export function ColorPicker({ value, onChange, label, description, disabled }: ColorPickerProps) {
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const [localValue, setLocalValue] = useState(value)
|
const [localValue, setLocalValue] = useState(value)
|
||||||
|
const [rgb, setRgb] = useState(() => hexToRgb(value))
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
const colorInputRef = useRef<HTMLInputElement>(null)
|
const colorInputRef = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
// Memoize Telegram check to avoid recalculating
|
||||||
|
const isTelegram = useMemo(() => isTelegramWebApp(), [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLocalValue(value)
|
setLocalValue(value)
|
||||||
|
setRgb(hexToRgb(value))
|
||||||
}, [value])
|
}, [value])
|
||||||
|
|
||||||
|
// Handle RGB slider change
|
||||||
|
const handleRgbChange = useCallback((channel: 'r' | 'g' | 'b', val: number) => {
|
||||||
|
const newRgb = { ...rgb, [channel]: val }
|
||||||
|
setRgb(newRgb)
|
||||||
|
const hex = rgbToHex(newRgb.r, newRgb.g, newRgb.b)
|
||||||
|
setLocalValue(hex)
|
||||||
|
onChange(hex)
|
||||||
|
}, [rgb, onChange])
|
||||||
|
|
||||||
// Close on outside click
|
// Close on outside click
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
function handleClickOutside(event: MouseEvent) {
|
function handleClickOutside(event: MouseEvent) {
|
||||||
@@ -103,46 +143,107 @@ export function ColorPicker({ value, onChange, label, description, disabled }: C
|
|||||||
maxLength={7}
|
maxLength={7}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Hidden native color picker for direct access */}
|
{/* Native color picker - hidden in Telegram WebApp (causes crash) */}
|
||||||
<input
|
{!isTelegram && (
|
||||||
ref={colorInputRef}
|
<>
|
||||||
type="color"
|
<input
|
||||||
value={localValue || '#000000'}
|
ref={colorInputRef}
|
||||||
onChange={handleColorInputChange}
|
type="color"
|
||||||
disabled={disabled}
|
value={localValue || '#000000'}
|
||||||
className="sr-only"
|
onChange={handleColorInputChange}
|
||||||
/>
|
disabled={disabled}
|
||||||
|
className="sr-only"
|
||||||
{/* Native picker button - min 44px for touch accessibility */}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => colorInputRef.current?.click()}
|
|
||||||
disabled={disabled}
|
|
||||||
className="btn-secondary min-w-[44px] min-h-[44px] p-2.5 disabled:opacity-50"
|
|
||||||
title="Open color picker"
|
|
||||||
aria-label="Open color picker"
|
|
||||||
>
|
|
||||||
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
|
||||||
<path
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
d="M4.098 19.902a3.75 3.75 0 005.304 0l6.401-6.402M6.75 21A3.75 3.75 0 013 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072M6.75 21a3.75 3.75 0 003.75-3.75V8.197M6.75 21h13.125c.621 0 1.125-.504 1.125-1.125v-5.25c0-.621-.504-1.125-1.125-1.125h-4.072M10.5 8.197l2.88-2.88c.438-.439 1.15-.439 1.59 0l3.712 3.713c.44.44.44 1.152 0 1.59l-2.879 2.88M6.75 17.25h.008v.008H6.75v-.008z"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
|
||||||
</button>
|
{/* Native picker button - min 44px for touch accessibility */}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => colorInputRef.current?.click()}
|
||||||
|
disabled={disabled}
|
||||||
|
className="btn-secondary min-w-[44px] min-h-[44px] p-2.5 disabled:opacity-50"
|
||||||
|
title="Open color picker"
|
||||||
|
aria-label="Open color picker"
|
||||||
|
>
|
||||||
|
<svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M4.098 19.902a3.75 3.75 0 005.304 0l6.401-6.402M6.75 21A3.75 3.75 0 013 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072M6.75 21a3.75 3.75 0 003.75-3.75V8.197M6.75 21h13.125c.621 0 1.125-.504 1.125-1.125v-5.25c0-.621-.504-1.125-1.125-1.125h-4.072M10.5 8.197l2.88-2.88c.438-.439 1.15-.439 1.59 0l3.712 3.713c.44.44.44 1.152 0 1.59l-2.879 2.88M6.75 17.25h.008v.008H6.75v-.008z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Dropdown with presets */}
|
{/* Dropdown with presets and RGB sliders */}
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="absolute z-50 mt-2 p-3 bg-dark-800 rounded-xl border border-dark-700 shadow-xl animate-fade-in">
|
<div className="absolute z-50 mt-2 p-3 bg-dark-800 rounded-xl border border-dark-700 shadow-xl animate-fade-in w-72">
|
||||||
|
{/* RGB Sliders - shown in Telegram instead of native picker */}
|
||||||
|
{isTelegram && (
|
||||||
|
<div className="mb-3 pb-3 border-b border-dark-700">
|
||||||
|
<div className="text-xs text-dark-400 mb-2">RGB</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{/* Red */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-red-400 w-4">R</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="255"
|
||||||
|
value={rgb.r}
|
||||||
|
onChange={(e) => handleRgbChange('r', parseInt(e.target.value))}
|
||||||
|
className="flex-1 h-2 rounded-full appearance-none cursor-pointer"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(to right, rgb(0,${rgb.g},${rgb.b}), rgb(255,${rgb.g},${rgb.b}))`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-dark-400 w-8 text-right">{rgb.r}</span>
|
||||||
|
</div>
|
||||||
|
{/* Green */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-green-400 w-4">G</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="255"
|
||||||
|
value={rgb.g}
|
||||||
|
onChange={(e) => handleRgbChange('g', parseInt(e.target.value))}
|
||||||
|
className="flex-1 h-2 rounded-full appearance-none cursor-pointer"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(to right, rgb(${rgb.r},0,${rgb.b}), rgb(${rgb.r},255,${rgb.b}))`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-dark-400 w-8 text-right">{rgb.g}</span>
|
||||||
|
</div>
|
||||||
|
{/* Blue */}
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-blue-400 w-4">B</span>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="255"
|
||||||
|
value={rgb.b}
|
||||||
|
onChange={(e) => handleRgbChange('b', parseInt(e.target.value))}
|
||||||
|
className="flex-1 h-2 rounded-full appearance-none cursor-pointer"
|
||||||
|
style={{
|
||||||
|
background: `linear-gradient(to right, rgb(${rgb.r},${rgb.g},0), rgb(${rgb.r},${rgb.g},255))`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<span className="text-xs text-dark-400 w-8 text-right">{rgb.b}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="text-xs text-dark-400 mb-2">Preset colors</div>
|
<div className="text-xs text-dark-400 mb-2">Preset colors</div>
|
||||||
<div className="grid grid-cols-4 sm:grid-cols-6 gap-1">
|
<div className="grid grid-cols-4 gap-1">
|
||||||
{PRESET_COLORS.map((preset) => (
|
{PRESET_COLORS.map((preset) => (
|
||||||
<button
|
<button
|
||||||
key={preset}
|
key={preset}
|
||||||
onClick={() => handlePresetClick(preset)}
|
onClick={() => handlePresetClick(preset)}
|
||||||
className={`min-w-[44px] min-h-[44px] w-11 h-11 rounded-lg border-2 transition-all hover:scale-105 ${
|
className={`min-w-[44px] min-h-[44px] w-full aspect-square rounded-lg border-2 transition-all hover:scale-105 ${
|
||||||
localValue === preset ? 'border-white ring-2 ring-white/30' : 'border-dark-600 hover:border-dark-500'
|
localValue.toLowerCase() === preset.toLowerCase() ? 'border-white ring-2 ring-white/30' : 'border-dark-600 hover:border-dark-500'
|
||||||
}`}
|
}`}
|
||||||
style={{ backgroundColor: preset }}
|
style={{ backgroundColor: preset }}
|
||||||
title={preset}
|
title={preset}
|
||||||
|
|||||||
@@ -88,19 +88,12 @@ export default function PromoDiscountBadge() {
|
|||||||
{/* Badge button */}
|
{/* Badge button */}
|
||||||
<button
|
<button
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
className="relative flex items-center gap-1.5 px-3 py-2 rounded-xl bg-gradient-to-r from-success-500/20 to-accent-500/20 border border-success-500/30 hover:border-success-500/50 transition-all group"
|
className="relative flex items-center gap-1 px-2.5 py-1.5 rounded-lg bg-success-500/15 hover:bg-success-500/25 transition-all group"
|
||||||
title={t('promo.activeDiscount', 'Active discount')}
|
title={t('promo.activeDiscount', 'Active discount')}
|
||||||
>
|
>
|
||||||
{/* Animated sparkle effect */}
|
<span className="font-bold text-success-400 text-sm">
|
||||||
<div className="absolute inset-0 rounded-xl bg-gradient-to-r from-success-500/10 to-accent-500/10 animate-pulse" />
|
|
||||||
|
|
||||||
<SparklesIcon />
|
|
||||||
<span className="relative font-bold text-success-400 text-sm">
|
|
||||||
-{activeDiscount.discount_percent}%
|
-{activeDiscount.discount_percent}%
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{/* Notification dot */}
|
|
||||||
<span className="absolute -top-1 -right-1 w-2 h-2 bg-success-400 rounded-full animate-pulse" />
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Dropdown - mobile: fixed centered, desktop: absolute right */}
|
{/* Dropdown - mobile: fixed centered, desktop: absolute right */}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ticketNotificationsApi } from '../api/ticketNotifications'
|
|||||||
import { useAuthStore } from '../store/auth'
|
import { useAuthStore } from '../store/auth'
|
||||||
import { useToast } from './Toast'
|
import { useToast } from './Toast'
|
||||||
import { useWebSocket, WSMessage } from '../hooks/useWebSocket'
|
import { useWebSocket, WSMessage } from '../hooks/useWebSocket'
|
||||||
|
import { useTelegramWebApp } from '../hooks/useTelegramWebApp'
|
||||||
import type { TicketNotification } from '../types'
|
import type { TicketNotification } from '../types'
|
||||||
|
|
||||||
const BellIcon = () => (
|
const BellIcon = () => (
|
||||||
@@ -32,6 +33,12 @@ export default function TicketNotificationBell({ isAdmin = false }: TicketNotifi
|
|||||||
const { showToast } = useToast()
|
const { showToast } = useToast()
|
||||||
const [isOpen, setIsOpen] = useState(false)
|
const [isOpen, setIsOpen] = useState(false)
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null)
|
const dropdownRef = useRef<HTMLDivElement>(null)
|
||||||
|
const { isFullscreen, safeAreaInset, contentSafeAreaInset } = useTelegramWebApp()
|
||||||
|
|
||||||
|
// Calculate dropdown top position (account for fullscreen safe area + TG buttons)
|
||||||
|
const dropdownTop = isFullscreen
|
||||||
|
? Math.max(safeAreaInset.top, contentSafeAreaInset.top) + 45 + 64 // safe area + TG buttons + header
|
||||||
|
: 64 // default header height
|
||||||
|
|
||||||
// Show toast for WebSocket notification
|
// Show toast for WebSocket notification
|
||||||
const showWSNotificationToast = useCallback((message: WSMessage) => {
|
const showWSNotificationToast = useCallback((message: WSMessage) => {
|
||||||
@@ -204,7 +211,12 @@ export default function TicketNotificationBell({ isAdmin = false }: TicketNotifi
|
|||||||
|
|
||||||
{/* Dropdown */}
|
{/* Dropdown */}
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="fixed sm:absolute top-16 sm:top-auto right-4 sm:right-0 left-4 sm:left-auto mt-0 sm:mt-2 w-auto sm:w-96 bg-dark-900/95 backdrop-blur-xl border border-dark-700/50 rounded-2xl shadow-2xl shadow-black/30 overflow-hidden z-50 animate-scale-in">
|
<div
|
||||||
|
className={`fixed sm:absolute sm:top-auto right-4 sm:right-0 left-4 sm:left-auto mt-0 sm:mt-2 w-auto sm:w-96 bg-dark-900/95 backdrop-blur-xl border border-dark-700/50 rounded-2xl shadow-2xl shadow-black/30 overflow-hidden z-50 animate-scale-in ${
|
||||||
|
!isFullscreen ? 'top-16' : ''
|
||||||
|
}`}
|
||||||
|
style={isFullscreen ? { top: `${dropdownTop}px` } : undefined}
|
||||||
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between px-4 py-3 border-b border-dark-700/50 bg-dark-800/30">
|
<div className="flex items-center justify-between px-4 py-3 border-b border-dark-700/50 bg-dark-800/30">
|
||||||
<h3 className="text-sm font-semibold text-dark-100">
|
<h3 className="text-sm font-semibold text-dark-100">
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||||
const { toggleTheme, isDark } = useTheme()
|
const { toggleTheme, isDark } = useTheme()
|
||||||
const [userPhotoUrl, setUserPhotoUrl] = useState<string | null>(null)
|
const [userPhotoUrl, setUserPhotoUrl] = useState<string | null>(null)
|
||||||
const { isTelegramWebApp, isFullscreen, isFullscreenSupported, toggleFullscreen } = useTelegramWebApp()
|
const { isTelegramWebApp, isFullscreen, isFullscreenSupported, toggleFullscreen, safeAreaInset, contentSafeAreaInset } = useTelegramWebApp()
|
||||||
|
|
||||||
// Fetch enabled themes from API - same source of truth as AdminSettings
|
// Fetch enabled themes from API - same source of truth as AdminSettings
|
||||||
const { data: enabledThemes } = useQuery({
|
const { data: enabledThemes } = useQuery({
|
||||||
@@ -300,7 +300,13 @@ export default function Layout({ children }: LayoutProps) {
|
|||||||
<AnimatedBackground />
|
<AnimatedBackground />
|
||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<header className="sticky top-0 z-50 glass border-b border-dark-800/50">
|
<header
|
||||||
|
className="sticky top-0 z-50 glass border-b border-dark-800/50"
|
||||||
|
style={{
|
||||||
|
// In fullscreen mode, add padding for safe area + Telegram native controls (close/menu buttons in corners)
|
||||||
|
paddingTop: isFullscreen ? `${Math.max(safeAreaInset.top, contentSafeAreaInset.top) + 45}px` : undefined,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="w-full mx-auto px-4 sm:px-6">
|
<div className="w-full mx-auto px-4 sm:px-6">
|
||||||
<div className="flex justify-between items-center h-16 lg:h-20">
|
<div className="flex justify-between items-center h-16 lg:h-20">
|
||||||
{/* Logo */}
|
{/* Logo */}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export function useTelegramWebApp() {
|
|||||||
const [isFullscreen, setIsFullscreen] = useState(false)
|
const [isFullscreen, setIsFullscreen] = useState(false)
|
||||||
const [isTelegramWebApp, setIsTelegramWebApp] = useState(false)
|
const [isTelegramWebApp, setIsTelegramWebApp] = useState(false)
|
||||||
const [safeAreaInset, setSafeAreaInset] = useState({ top: 0, bottom: 0, left: 0, right: 0 })
|
const [safeAreaInset, setSafeAreaInset] = useState({ top: 0, bottom: 0, left: 0, right: 0 })
|
||||||
|
const [contentSafeAreaInset, setContentSafeAreaInset] = useState({ top: 0, bottom: 0, left: 0, right: 0 })
|
||||||
|
|
||||||
const webApp = typeof window !== 'undefined' ? window.Telegram?.WebApp : undefined
|
const webApp = typeof window !== 'undefined' ? window.Telegram?.WebApp : undefined
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ export function useTelegramWebApp() {
|
|||||||
setIsTelegramWebApp(true)
|
setIsTelegramWebApp(true)
|
||||||
setIsFullscreen(webApp.isFullscreen || false)
|
setIsFullscreen(webApp.isFullscreen || false)
|
||||||
setSafeAreaInset(webApp.safeAreaInset || { top: 0, bottom: 0, left: 0, right: 0 })
|
setSafeAreaInset(webApp.safeAreaInset || { top: 0, bottom: 0, left: 0, right: 0 })
|
||||||
|
setContentSafeAreaInset(webApp.contentSafeAreaInset || { top: 0, bottom: 0, left: 0, right: 0 })
|
||||||
|
|
||||||
// Expand WebApp to full height
|
// Expand WebApp to full height
|
||||||
webApp.expand()
|
webApp.expand()
|
||||||
@@ -29,12 +31,23 @@ export function useTelegramWebApp() {
|
|||||||
const handleFullscreenChanged = () => {
|
const handleFullscreenChanged = () => {
|
||||||
setIsFullscreen(webApp.isFullscreen || false)
|
setIsFullscreen(webApp.isFullscreen || false)
|
||||||
setSafeAreaInset(webApp.safeAreaInset || { top: 0, bottom: 0, left: 0, right: 0 })
|
setSafeAreaInset(webApp.safeAreaInset || { top: 0, bottom: 0, left: 0, right: 0 })
|
||||||
|
setContentSafeAreaInset(webApp.contentSafeAreaInset || { top: 0, bottom: 0, left: 0, right: 0 })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for safe area changes
|
||||||
|
const handleSafeAreaChanged = () => {
|
||||||
|
setSafeAreaInset(webApp.safeAreaInset || { top: 0, bottom: 0, left: 0, right: 0 })
|
||||||
|
setContentSafeAreaInset(webApp.contentSafeAreaInset || { top: 0, bottom: 0, left: 0, right: 0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
webApp.onEvent('fullscreenChanged', handleFullscreenChanged)
|
webApp.onEvent('fullscreenChanged', handleFullscreenChanged)
|
||||||
|
webApp.onEvent('safeAreaChanged', handleSafeAreaChanged)
|
||||||
|
webApp.onEvent('contentSafeAreaChanged', handleSafeAreaChanged)
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
webApp.offEvent('fullscreenChanged', handleFullscreenChanged)
|
webApp.offEvent('fullscreenChanged', handleFullscreenChanged)
|
||||||
|
webApp.offEvent('safeAreaChanged', handleSafeAreaChanged)
|
||||||
|
webApp.offEvent('contentSafeAreaChanged', handleSafeAreaChanged)
|
||||||
}
|
}
|
||||||
}, [webApp])
|
}, [webApp])
|
||||||
|
|
||||||
@@ -86,6 +99,7 @@ export function useTelegramWebApp() {
|
|||||||
isFullscreen,
|
isFullscreen,
|
||||||
isFullscreenSupported,
|
isFullscreenSupported,
|
||||||
safeAreaInset,
|
safeAreaInset,
|
||||||
|
contentSafeAreaInset,
|
||||||
requestFullscreen,
|
requestFullscreen,
|
||||||
exitFullscreen,
|
exitFullscreen,
|
||||||
toggleFullscreen,
|
toggleFullscreen,
|
||||||
|
|||||||
@@ -102,19 +102,34 @@
|
|||||||
|
|
||||||
html {
|
html {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
scroll-behavior: smooth;
|
}
|
||||||
|
|
||||||
|
/* Smooth scroll only on desktop - mobile uses native */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark theme (default) */
|
/* Dark theme (default) */
|
||||||
body, .dark body {
|
body, .dark body {
|
||||||
@apply bg-dark-950 text-dark-100 font-sans antialiased;
|
@apply bg-dark-950 text-dark-100 font-sans antialiased;
|
||||||
overscroll-behavior-y: contain;
|
overscroll-behavior-y: contain;
|
||||||
|
/* iOS smooth scrolling */
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optimize main scrollable content */
|
||||||
|
main {
|
||||||
|
/* Prevent layout shifts during scroll */
|
||||||
|
contain: layout style;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light theme - Champagne */
|
/* Light theme - Champagne */
|
||||||
.light body {
|
.light body {
|
||||||
@apply bg-champagne-200 text-champagne-900 font-sans antialiased;
|
@apply bg-champagne-200 text-champagne-900 font-sans antialiased;
|
||||||
overscroll-behavior-y: contain;
|
overscroll-behavior-y: contain;
|
||||||
|
-webkit-overflow-scrolling: touch;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light theme text color overrides - convert dark-* text colors to readable colors */
|
/* Light theme text color overrides - convert dark-* text colors to readable colors */
|
||||||
@@ -310,10 +325,19 @@
|
|||||||
@layer components {
|
@layer components {
|
||||||
/* ========== DARK THEME COMPONENTS (default) ========== */
|
/* ========== DARK THEME COMPONENTS (default) ========== */
|
||||||
|
|
||||||
/* Cards - Dark */
|
/* Cards - Dark (optimized for mobile) */
|
||||||
.card {
|
.card {
|
||||||
@apply bg-dark-900/50 backdrop-blur-sm rounded-2xl border border-dark-800/50 p-5 sm:p-6
|
@apply bg-dark-900/70 rounded-2xl border border-dark-800/50 p-5 sm:p-6
|
||||||
transition-all duration-300 ease-smooth;
|
transition-all duration-300 ease-smooth;
|
||||||
|
/* GPU acceleration for smooth scroll */
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enable backdrop-blur only on desktop */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.card {
|
||||||
|
@apply bg-dark-900/50 backdrop-blur-sm;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-hover {
|
.card-hover {
|
||||||
@@ -329,9 +353,19 @@
|
|||||||
@apply card animate-slide-up;
|
@apply card animate-slide-up;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Glass effect - Dark */
|
/* Glass effect - Dark (optimized for mobile) */
|
||||||
.glass {
|
.glass {
|
||||||
@apply bg-dark-900/30 backdrop-blur-xl border border-dark-700/30;
|
@apply bg-dark-900/80 border border-dark-700/30;
|
||||||
|
/* GPU acceleration */
|
||||||
|
transform: translateZ(0);
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Enable backdrop-blur only on desktop where it's performant */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.glass {
|
||||||
|
@apply bg-dark-900/30 backdrop-blur-xl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Buttons - Dark */
|
/* Buttons - Dark */
|
||||||
@@ -455,11 +489,18 @@
|
|||||||
@apply nav-item text-accent-400 bg-accent-500/10;
|
@apply nav-item text-accent-400 bg-accent-500/10;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bottom nav - Dark */
|
/* Bottom nav - Dark (optimized for mobile) */
|
||||||
.bottom-nav {
|
.bottom-nav {
|
||||||
@apply fixed bottom-0 left-0 right-0 z-50
|
@apply fixed bottom-0 left-0 right-0 z-50
|
||||||
bg-dark-900/80 backdrop-blur-xl border-t border-dark-800/50
|
bg-dark-900/95 border-t border-dark-800/50
|
||||||
safe-area-pb;
|
safe-area-pb;
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.bottom-nav {
|
||||||
|
@apply bg-dark-900/80 backdrop-blur-xl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bottom-nav-item {
|
.bottom-nav-item {
|
||||||
@@ -476,12 +517,34 @@
|
|||||||
@apply border-t border-dark-800/50;
|
@apply border-t border-dark-800/50;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========== MOBILE PERFORMANCE: Disable backdrop-blur ========== */
|
||||||
|
@media (max-width: 1023px) {
|
||||||
|
/* Disable all backdrop-blur on mobile for better scroll performance */
|
||||||
|
.backdrop-blur,
|
||||||
|
.backdrop-blur-sm,
|
||||||
|
.backdrop-blur-md,
|
||||||
|
.backdrop-blur-lg,
|
||||||
|
.backdrop-blur-xl,
|
||||||
|
.backdrop-blur-2xl,
|
||||||
|
.backdrop-blur-3xl {
|
||||||
|
backdrop-filter: none !important;
|
||||||
|
-webkit-backdrop-filter: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ========== LIGHT THEME COMPONENTS (Champagne) ========== */
|
/* ========== LIGHT THEME COMPONENTS (Champagne) ========== */
|
||||||
|
|
||||||
/* Cards - Light */
|
/* Cards - Light */
|
||||||
.light .card {
|
.light .card {
|
||||||
@apply bg-white/80 backdrop-blur-sm rounded-2xl border border-champagne-300/50 p-5 sm:p-6
|
@apply bg-white/90 rounded-2xl border border-champagne-300/50 p-5 sm:p-6
|
||||||
shadow-sm;
|
shadow-sm;
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.light .card {
|
||||||
|
@apply bg-white/80 backdrop-blur-sm;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.light .card-hover {
|
.light .card-hover {
|
||||||
@@ -492,9 +555,16 @@
|
|||||||
@apply hover:shadow-lg hover:border-accent-400/40;
|
@apply hover:shadow-lg hover:border-accent-400/40;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Glass effect - Light */
|
/* Glass effect - Light (optimized for mobile) */
|
||||||
.light .glass {
|
.light .glass {
|
||||||
@apply bg-white/60 backdrop-blur-xl border border-champagne-300/40;
|
@apply bg-white/90 border border-champagne-300/40;
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.light .glass {
|
||||||
|
@apply bg-white/60 backdrop-blur-xl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Buttons - Light */
|
/* Buttons - Light */
|
||||||
@@ -884,6 +954,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Color picker range input styling */
|
||||||
|
input[type="range"] {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-webkit-slider-thumb {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
appearance: none;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid rgba(0, 0, 0, 0.2);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"]::-moz-range-thumb {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid rgba(0, 0, 0, 0.2);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
/* Selection color */
|
/* Selection color */
|
||||||
::selection {
|
::selection {
|
||||||
@apply bg-accent-500/30 text-dark-50;
|
@apply bg-accent-500/30 text-dark-50;
|
||||||
|
|||||||
Reference in New Issue
Block a user