diff --git a/.ai/BENTO_REFACTOR.md b/.ai/BENTO_REFACTOR.md new file mode 100644 index 0000000..93d1e71 --- /dev/null +++ b/.ai/BENTO_REFACTOR.md @@ -0,0 +1,150 @@ +# Bento Grids Refactor + +> **Ветка:** `bento-grids` +> **Статус:** В работе +> **Последнее обновление:** 2026-01-20 + +## Референс + +- **Стиль:** Bento Grids (карточки разных размеров в сетке) +- **Цветовая схема:** Зеленый неон на темном фоне, чистота +- **Шрифт:** Urbanist (современный гротеск) +- **Скругления:** Крупные (24-32px) + +--- + +## План рефакторинга + +### Этап 1: Фундамент (Vibe Check) +> Цель: Пустая страница уже выглядит "секси" + +| # | Задача | Статус | Заметки | +|---|--------|--------|---------| +| 1.1 | Подключить Urbanist шрифт | `[x]` | Google Fonts, заменить Inter | +| 1.2 | Tailwind: bento radius/spacing | `[x]` | `rounded-bento`, `rounded-4xl`, `spacing.bento` | +| 1.3 | globals.css: CSS переменные Bento | `[x]` | `--bento-radius`, `--bento-gap`, `--bento-padding` | + +**Файлы:** +- `src/styles/globals.css` +- `tailwind.config.js` + +--- + +### Этап 2: Атом (BentoCard) +> Цель: Один идеальный компонент-карточка + +| # | Задача | Статус | Заметки | +|---|--------|--------|---------| +| 2.1 | Создать BentoCard компонент | `[x]` | С вариантами размеров | +| 2.2 | Обновить .card в globals.css | `[x]` | Новые классы .bento-card, .bento-card-hover, .bento-card-glow | + +**Файлы:** +- `src/components/ui/BentoCard.tsx` (новый) +- `src/styles/globals.css` + +**API компонента:** +```tsx +type BentoSize = 'sm' | 'md' | 'lg' | 'xl' // 1x1, 2x1, 1x2, 2x2 + +interface BentoCardProps { + size?: BentoSize + children: React.ReactNode + className?: string + hover?: boolean + as?: 'div' | 'Link' | 'button' +} +``` + +--- + +### Этап 3: Скелет (Floating TabBar) +> Цель: Bottom navigation как "парящий остров" + +| # | Задача | Статус | Заметки | +|---|--------|--------|---------| +| 3.1 | Floating island стиль | `[x]` | margin 16px от краев, shadow | +| 3.2 | Active state с pill | `[x]` | rounded-2xl фон под активной иконкой | +| 3.3 | Убрать border-top | `[x]` | Заменено на shadow | + +**Файлы:** +- `src/components/layout/Layout.tsx` (секция bottom-nav) +- `src/styles/globals.css` (`.bottom-nav` классы) + +--- + +### Этап 4: Мясо (Dashboard Grid) +> Цель: Главная страница в стиле Bento + +| # | Задача | Статус | Заметки | +|---|--------|--------|---------| +| 4.1 | Создать .bento-grid контейнер | `[x]` | CSS Grid с gap, 2 колонки mobile, 4 desktop | +| 4.2 | Subscription Status → bento-card | `[x]` | Главная карточка подписки | +| 4.3 | Stats Grid → bento-grid + bento-card-hover | `[x]` | Balance, Subscription, Referrals, Earnings | +| 4.4 | Quick Actions → bento-card | `[x]` | Контейнер для кнопок | +| 4.5 | Trial + Wheel Banner → bento-card-glow/hover | `[x]` | Акцентные карточки | + +**Файлы:** +- `src/pages/Dashboard.tsx` +- `src/styles/globals.css` + +**Правило:** Логику JS/TS НЕ трогаем — только UI обёртки. + +--- + +## Решения и обоснования + +### Почему Urbanist, а не Inter? +- Urbanist более геометричный, лучше подходит под Bento-эстетику +- Хорошая читаемость на мобильных +- Бесплатный, Google Fonts + +### Почему rounded-3xl (24px)? +- Стандарт Bento — крупные скругления +- 16px (текущий rounded-2xl) выглядит слишком "приложенечно" +- 32px (rounded-4xl) для особо крупных элементов + +### Floating TabBar vs прилипший +- Floating выглядит премиально +- Отделяет навигацию от контента визуально +- Работает с safe-area на iOS + +--- + +## Что НЕ делаем + +- ❌ Модалки (ConnectionModal, TopUpModal) — вторично +- ❌ Страницы кроме Dashboard — после MVP +- ❌ Header — работает, не ломаем +- ❌ Рефакторинг логики — только UI + +--- + +## Прогресс + +``` +Этап 1: ██████████ 100% +Этап 2: ██████████ 100% +Этап 3: ██████████ 100% +Этап 4: ██████████ 100% +───────────────────── +Общий: ██████████ 100% +``` + +--- + +## Заметки для агентов + +**Контекст:** Это Mini App для Telegram (VPN кабинет). Мобильный фокус. + +**Текущий стек:** +- React + Vite + TypeScript +- Tailwind CSS +- React Query для данных + +**Ключевые файлы:** +- `tailwind.config.js` — цвета через CSS переменные +- `src/styles/globals.css` — компонентные классы (.card, .btn, etc) +- `src/components/layout/Layout.tsx` — шапка + таббар +- `src/pages/Dashboard.tsx` — главный экран (монстр, но логику не трогаем) + +**Темы:** Есть dark и light (champagne). Bento-стили должны работать в обеих. diff --git a/src/components/ui/BentoCard.tsx b/src/components/ui/BentoCard.tsx new file mode 100644 index 0000000..3f9d544 --- /dev/null +++ b/src/components/ui/BentoCard.tsx @@ -0,0 +1,115 @@ +import { Link } from 'react-router-dom' +import { forwardRef } from 'react' + +export type BentoSize = 'sm' | 'md' | 'lg' | 'xl' + +interface BentoCardBaseProps { + size?: BentoSize + children: React.ReactNode + className?: string + hover?: boolean + glow?: boolean +} + +interface BentoCardDivProps extends BentoCardBaseProps { + as?: 'div' + onClick?: () => void +} + +interface BentoCardLinkProps extends BentoCardBaseProps { + as: 'link' + to: string + state?: unknown +} + +interface BentoCardButtonProps extends BentoCardBaseProps { + as: 'button' + onClick?: () => void + disabled?: boolean + type?: 'button' | 'submit' +} + +export type BentoCardProps = BentoCardDivProps | BentoCardLinkProps | BentoCardButtonProps + +const sizeClasses: Record = { + sm: '', + md: 'col-span-2', + lg: 'row-span-2', + xl: 'col-span-2 row-span-2', +} + +const baseClasses = ` + bento-card + rounded-[var(--bento-radius)] + p-[var(--bento-padding)] + bg-dark-900/70 + border border-dark-700/40 + transition-all duration-300 ease-smooth +` + +const hoverClasses = ` + cursor-pointer + hover:bg-dark-800/60 + hover:border-dark-600/50 + hover:shadow-lg + hover:scale-[1.01] + active:scale-[0.99] +` + +const glowClasses = ` + hover:shadow-glow + hover:border-accent-500/30 +` + +export const BentoCard = forwardRef((props, ref) => { + const { + size = 'sm', + children, + className = '', + hover = false, + glow = false, + } = props + + const classes = [ + baseClasses, + sizeClasses[size], + hover && hoverClasses, + glow && glowClasses, + className, + ].filter(Boolean).join(' ') + + if (props.as === 'link') { + const { to, state } = props as BentoCardLinkProps + return ( + + {children} + + ) + } + + if (props.as === 'button') { + const { onClick, disabled, type = 'button' } = props as BentoCardButtonProps + return ( + + ) + } + + const { onClick } = props as BentoCardDivProps + return ( +
+ {children} +
+ ) +}) + +BentoCard.displayName = 'BentoCard' + +export default BentoCard diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 2c20823..99880ae 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -254,7 +254,7 @@ export default function Dashboard() { {/* Subscription Status - Main Card */} {subscription && ( -
+

{t('subscription.status')}

@@ -336,9 +336,9 @@ export default function Dashboard() { )} {/* Stats Grid */} -
+
{/* Balance */} - +
{t('balance.currentBalance')} @@ -352,7 +352,7 @@ export default function Dashboard() { {/* Subscription */} - +
{t('subscription.title')} @@ -388,7 +388,7 @@ export default function Dashboard() { {/* Referrals */} - +
{t('referral.stats.totalReferrals')} @@ -403,7 +403,7 @@ export default function Dashboard() { {/* Earnings */} - +
{t('referral.stats.totalEarnings')} @@ -422,7 +422,7 @@ export default function Dashboard() { {/* Trial Activation */} {hasNoSubscription && !trialLoading && trialInfo?.is_available && ( -
+
@@ -483,7 +483,7 @@ export default function Dashboard() { {wheelConfig?.is_enabled && (
{/* Emoji */} @@ -504,7 +504,7 @@ export default function Dashboard() { )} {/* Quick Actions */} -
+

{t('dashboard.quickActions')}

diff --git a/src/styles/globals.css b/src/styles/globals.css index e756480..d434854 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -1,4 +1,4 @@ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap'); +@import url('https://fonts.googleapis.com/css2?family=Urbanist:wght@400;500;600;700;800&display=swap'); @tailwind base; @tailwind components; @@ -8,6 +8,14 @@ :root { --safe-area-inset-bottom: env(safe-area-inset-bottom, 0px); + /* Bento Design System */ + --bento-radius: 24px; + --bento-radius-lg: 32px; + --bento-gap: 16px; + --bento-gap-lg: 24px; + --bento-padding: 24px; + --bento-padding-lg: 32px; + /* Theme colors - RGB format for opacity support */ /* Dark palette (RGB values) */ --color-dark-50: 248, 250, 252; @@ -327,6 +335,63 @@ } @layer components { + /* ========== BENTO DESIGN SYSTEM ========== */ + + .bento-card { + @apply bg-dark-900/70 border border-dark-700/40 + transition-all duration-300 ease-smooth; + border-radius: var(--bento-radius); + padding: var(--bento-padding); + transform: translateZ(0); + } + + @media (min-width: 1024px) { + .bento-card { + @apply bg-dark-900/50 backdrop-blur-sm; + } + } + + .bento-card-hover { + @apply bento-card cursor-pointer + hover:bg-dark-800/60 hover:border-dark-600/50 + hover:shadow-lg hover:scale-[1.01] active:scale-[0.99]; + } + + .bento-card-glow { + @apply bento-card hover:shadow-glow hover:border-accent-500/30 hover:scale-[1.01]; + } + + .bento-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: var(--bento-gap); + } + + @media (min-width: 768px) { + .bento-grid { + grid-template-columns: repeat(4, 1fr); + gap: var(--bento-gap-lg); + } + } + + .light .bento-card { + @apply bg-white/90 border-champagne-300/50 shadow-sm; + } + + @media (min-width: 1024px) { + .light .bento-card { + @apply bg-white/80 backdrop-blur-sm; + } + } + + .light .bento-card-hover { + @apply hover:bg-white hover:border-champagne-400/50 hover:shadow-md; + } + + .light .bento-card-glow { + @apply hover:shadow-lg hover:border-accent-400/40; + } + /* ========== DARK THEME COMPONENTS (default) ========== */ /* Cards - Dark (optimized for mobile) */ @@ -493,12 +558,17 @@ @apply nav-item text-accent-400 bg-accent-500/10; } - /* Bottom nav - Dark (optimized for mobile) */ .bottom-nav { - @apply fixed bottom-0 left-0 right-0 z-50 - bg-dark-900/95 border-t border-dark-800/50 - safe-area-pb; + @apply fixed z-50 bg-dark-900/95; + bottom: 16px; + left: 16px; + right: 16px; + border-radius: var(--bento-radius); + padding: 8px 4px; + padding-bottom: calc(8px + var(--safe-area-inset-bottom)); transform: translateZ(0); + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.4), + 0 0 0 1px rgba(255, 255, 255, 0.05) inset; } @media (min-width: 1024px) { @@ -508,12 +578,17 @@ } .bottom-nav-item { - @apply flex flex-col items-center justify-center py-2 px-2 flex-1 min-w-[60px] - text-dark-500 transition-colors duration-200 shrink-0; + @apply flex flex-col items-center justify-center py-2.5 px-3 flex-1 min-w-[56px] + text-dark-500 transition-all duration-200 shrink-0 rounded-2xl; + } + + .bottom-nav-item:hover { + @apply text-dark-300; } .bottom-nav-item-active { - @apply bottom-nav-item text-accent-400; + @apply flex flex-col items-center justify-center py-2.5 px-3 flex-1 min-w-[56px] + text-accent-400 bg-accent-500/15 rounded-2xl transition-all duration-200 shrink-0; } /* Divider - Dark */ @@ -668,17 +743,28 @@ @apply text-champagne-800 bg-champagne-200/70; } - /* Bottom nav - Light */ .light .bottom-nav { - @apply bg-white/90 backdrop-blur-xl border-t border-champagne-200; + @apply bg-white/95; + box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1), + 0 0 0 1px rgba(0, 0, 0, 0.05); + } + + @media (min-width: 1024px) { + .light .bottom-nav { + @apply bg-white/80 backdrop-blur-xl; + } } .light .bottom-nav-item { @apply text-champagne-500; } + .light .bottom-nav-item:hover { + @apply text-champagne-700; + } + .light .bottom-nav-item-active { - @apply text-champagne-800; + @apply text-champagne-800 bg-champagne-300/40; } /* Divider - Light */ diff --git a/tailwind.config.js b/tailwind.config.js index b6d30c7..4460776 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -106,7 +106,15 @@ export default { }, }, fontFamily: { - sans: ['Inter', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'], + sans: ['Urbanist', 'system-ui', '-apple-system', 'BlinkMacSystemFont', 'Segoe UI', 'Roboto', 'sans-serif'], + }, + borderRadius: { + 'bento': '24px', + '4xl': '32px', + }, + spacing: { + 'bento': '16px', + 'bento-lg': '24px', }, fontSize: { '2xs': ['0.625rem', { lineHeight: '0.875rem' }],