feat(ui): implement Bento Grid design system

- Add Urbanist font (replace Inter)
- Add CSS variables: --bento-radius, --bento-gap, --bento-padding
- Add Tailwind extend: rounded-bento, rounded-4xl, spacing.bento
- Create BentoCard component with size variants (sm/md/lg/xl)
- Add .bento-card, .bento-card-hover, .bento-card-glow classes
- Add .bento-grid container with responsive columns
- Implement floating TabBar (margin 16px, shadow, pill active state)
- Apply bento styles to Dashboard cards
- Support both dark and light themes
This commit is contained in:
Dev
2026-01-20 21:10:20 +05:00
parent 9b0f5060dd
commit 6d88bac693
5 changed files with 380 additions and 21 deletions

View File

@@ -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<BentoSize, string> = {
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<HTMLDivElement, BentoCardProps>((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 (
<Link to={to} state={state} className={classes}>
{children}
</Link>
)
}
if (props.as === 'button') {
const { onClick, disabled, type = 'button' } = props as BentoCardButtonProps
return (
<button
ref={ref as React.Ref<HTMLButtonElement>}
type={type}
onClick={onClick}
disabled={disabled}
className={classes}
>
{children}
</button>
)
}
const { onClick } = props as BentoCardDivProps
return (
<div ref={ref} onClick={onClick} className={classes}>
{children}
</div>
)
})
BentoCard.displayName = 'BentoCard'
export default BentoCard

View File

@@ -254,7 +254,7 @@ export default function Dashboard() {
{/* Subscription Status - Main Card */}
{subscription && (
<div className="card">
<div className="bento-card">
<div className="flex items-center justify-between mb-6">
<h2 className="text-lg font-semibold text-dark-100">{t('subscription.status')}</h2>
<span className={subscription.is_active ? 'badge-success' : 'badge-error'}>
@@ -336,9 +336,9 @@ export default function Dashboard() {
)}
{/* Stats Grid */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div className="bento-grid">
{/* Balance */}
<Link to="/balance" className="card-hover group" data-onboarding="balance">
<Link to="/balance" className="bento-card-hover group" data-onboarding="balance">
<div className="flex items-center justify-between mb-3">
<span className="text-dark-400 text-sm">{t('balance.currentBalance')}</span>
<span className="text-dark-600 group-hover:text-accent-400 transition-colors">
@@ -352,7 +352,7 @@ export default function Dashboard() {
</Link>
{/* Subscription */}
<Link to="/subscription" className="card-hover group" data-onboarding="subscription-status">
<Link to="/subscription" className="bento-card-hover group" data-onboarding="subscription-status">
<div className="flex items-center justify-between mb-3">
<span className="text-dark-400 text-sm">{t('subscription.title')}</span>
<span className="text-dark-600 group-hover:text-accent-400 transition-colors">
@@ -388,7 +388,7 @@ export default function Dashboard() {
</Link>
{/* Referrals */}
<Link to="/referral" className="card-hover group">
<Link to="/referral" className="bento-card-hover group">
<div className="flex items-center justify-between mb-3">
<span className="text-dark-400 text-sm">{t('referral.stats.totalReferrals')}</span>
<span className="text-dark-600 group-hover:text-accent-400 transition-colors">
@@ -403,7 +403,7 @@ export default function Dashboard() {
</Link>
{/* Earnings */}
<Link to="/referral" className="card-hover group">
<Link to="/referral" className="bento-card-hover group">
<div className="flex items-center justify-between mb-3">
<span className="text-dark-400 text-sm">{t('referral.stats.totalEarnings')}</span>
<span className="text-dark-600 group-hover:text-accent-400 transition-colors">
@@ -422,7 +422,7 @@ export default function Dashboard() {
{/* Trial Activation */}
{hasNoSubscription && !trialLoading && trialInfo?.is_available && (
<div className="card border-accent-500/30 bg-gradient-to-br from-accent-500/5 to-transparent">
<div className="bento-card-glow border-accent-500/30 bg-gradient-to-br from-accent-500/5 to-transparent">
<div className="flex items-start gap-4">
<div className="w-12 h-12 rounded-xl bg-accent-500/20 flex items-center justify-center flex-shrink-0">
<SparklesIcon />
@@ -483,7 +483,7 @@ export default function Dashboard() {
{wheelConfig?.is_enabled && (
<Link
to="/wheel"
className="group card-hover flex items-center justify-between"
className="group bento-card-hover flex items-center justify-between"
>
<div className="flex items-center gap-4">
{/* Emoji */}
@@ -504,7 +504,7 @@ export default function Dashboard() {
)}
{/* Quick Actions */}
<div className="card" data-onboarding="quick-actions">
<div className="bento-card" data-onboarding="quick-actions">
<h3 className="text-lg font-semibold text-dark-100 mb-4">{t('dashboard.quickActions')}</h3>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
<Link to="/balance" className="btn-secondary justify-center text-center text-sm py-2.5">

View File

@@ -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 */