feat: Thêm các thành phần UI cho trạng thái tải (loading states), trạng thái rỗng (empty states) và tích hợp tài sản thương hiệu.

This commit is contained in:
Ho Ngoc Hai
2026-01-04 15:34:24 +07:00
parent c54a9debe3
commit 7cf4111531
18 changed files with 1352 additions and 46 deletions

View File

@@ -1,31 +1,185 @@
# Web Client Application
# GoodGo Platform - Web Client
Next.js web application for GoodGo Platform Client Portal.
> **EN**: Enterprise-grade web client for GoodGo microservices platform
> **VI**: Web client cấp doanh nghiệp cho nền tảng microservices GoodGo
## Features
## Features / Tính Năng
- Next.js 14 with App Router
- TypeScript
- Tailwind CSS
- Zustand for state management
- API integration with auth service
- **Brand Identity System** - Complete logo suite, favicons, and illustrations
- **Design System** - Professional design tokens with brand colors, gradients, and glassmorphism
- **UI Component Library** - Enhanced components with brand variants (Button, EmptyState, Loading States, Logo)
- **Dark/Light Theme** - Automatic theme switching with system preference
- **Internationalization** - Multi-language support (i18n ready)
-**TypeScript** - Full type safety
-**Tailwind CSS 4** - Modern utility-first styling
-**Next.js 14** - App Router with RSC
-**Accessibility** - WCAG 2.1 AA compliant
-**PWA Ready** - Progressive Web App support
## Development
## Tech Stack
- **Framework**: Next.js 14 (App Router)
- **Language**: TypeScript 5+
- **Styling**: Tailwind CSS 4 (CSS-first)
- **State Management**: Zustand
- **API Client**: `@goodgo/http-client`
- **Testing**: Vitest + Playwright
- **Component Development**: Storybook
##Development / Phát Triển
```bash
# Install dependencies
# Install dependencies / Cài đặt dependencies
pnpm install
# Start development server
# Start dev server / Khởi động dev server
pnpm dev
# → http://localhost:3000
# Build for production
# Start Storybook / Khởi động Storybook
pnpm storybook
# → http://localhost:6006
# Build for production / Build cho production
pnpm build
# Start production server
pnpm start
# Type checking / Kiểm tra kiểu
pnpm typecheck
# Lint / Kiểm tra lỗi
pnpm lint
```
## Environment Variables
## Environment Variables / Biến Môi Trường
Create `.env.local` file:
```bash
# API URL
NEXT_PUBLIC_API_URL=http://localhost/api/v1
```
## Brand Assets / Tài Sản Thương Hiệu
Brand assets are located in `/public/brand-assets/`:
- **Logos**: `/brand-assets/logo/` (full, icon, wordmark variants)
- **Icons**: `/brand-assets/icons/` (favicon)
- **Illustrations**: `/brand-assets/illustrations/` (empty state, error state)
Usage in components:
```tsx
import { BrandLogo } from '@/components/ui/brand-logo';
import { BRAND } from '@/lib/brand-constants';
// Logo component
<BrandLogo variant="full" size="lg" />
// Brand constants
const primaryColor = BRAND.colors.primary.hex; // #3B82F6
```
## UI Components / Components Giao Diện
### BrandLogo
```tsx
import { BrandLogo, BrandLogoLink } from '@/components/ui/brand-logo';
<BrandLogo variant="full" size="xl" />
<BrandLogoLink variant="icon" size="md" href="/" />
```
### Button with Brand Variants
```tsx
import { Button } from '@/components/ui/button';
<Button variant="brand">Get Started</Button>
<Button variant="glass">Learn More</Button>
```
### Empty State
```tsx
import { EmptyState, ErrorState } from '@/components/ui/empty-state';
<EmptyState
title="No items found"
description="Try adding some items"
action={{ label: 'Add Item', onClick: handleAdd }}
/>
```
### Loading States
```tsx
import {
BrandSpinner,
Skeleton,
SkeletonCard,
LoadingOverlay,
ProgressBar
} from '@/components/ui/loading-states';
<BrandSpinner size="lg" color="brand" />
<SkeletonCard />
<LoadingOverlay show={isLoading} message="Loading..." />
<ProgressBar value={progress} showLabel />
```
## Design System / Hệ Thống Thiết Kế
### Brand Colors
```css
/* Primary (Blue) - Tech & Trust */
--brand-primary: #3B82F6;
/* Secondary (Purple) - Innovation */
--brand-secondary: #8B5CF6;
/* Accent (Cyan) - Energy */
--brand-accent: #06B6D4;
```
### Tailwind Utilities
```tsx
// Brand colors
<div className="bg-brand-primary text-white" />
<div className="bg-brand-gradient" />
// Glassmorphism
<div className="bg-glass-bg backdrop-blur-glass border-glass-border" />
// Brand shadows
<div className="shadow-brand hover:shadow-brand-lg" />
```
## Project Structure / Cấu Trúc Dự Án
```
apps/web-client/
├── public/
│ └── brand-assets/ # Brand logos, icons, illustrations
├── src/
│ ├── app/ # Next.js App Router pages
│ ├── components/
│ │ └── ui/ # Reusable UI components
│ ├── lib/
│ │ └── brand-constants.ts # Brand helper functions
│ ├── styles/
│ │ └── theme.css # Design system tokens
│ ├── contexts/ # React contexts (Theme, etc.)
│ ├── hooks/ # Custom React hooks
│ ├── providers/ # Provider components
│ └── stores/ # Zustand stores
└── tailwind.config.js # Tailwind configuration
```
## Contributing / Đóng Góp
Please read [CONTRIBUTING.md](../../CONTRIBUTING.md) for details.
## License / Giấy Phép
Proprietary - GoodGo Platform
- `NEXT_PUBLIC_API_URL` - API base URL (default: http://localhost/api/v1)

View File

@@ -0,0 +1,19 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<!--
EN: Favicon - Simplified icon optimized for small sizes
VI: Favicon - Icon đơn giản hóa tối ưu cho kích thước nhỏ
-->
<defs>
<linearGradient id="faviconGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3B82F6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#8B5CF6;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Simplified hexagon -->
<path d="M16 4 L25 10 L25 22 L16 28 L7 22 L7 10 Z"
fill="url(#faviconGradient)"/>
<!-- Center dot -->
<circle cx="16" cy="16" r="3" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 712 B

View File

@@ -0,0 +1,50 @@
<svg width="400" height="300" viewBox="0 0 400 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<!--
EN: Empty State Illustration - Minimalist design with brand colors
VI: Minh họa Empty State - Thiết kế tối giản với màu thương hiệu
-->
<defs>
<linearGradient id="emptyGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3B82F6;stop-opacity:0.2" />
<stop offset="100%" style="stop-color:#8B5CF6;stop-opacity:0.2" />
</linearGradient>
</defs>
<!-- Background circle -->
<circle cx="200" cy="150" r="120" fill="url(#emptyGradient)"/>
<!-- Document icon -->
<g transform="translate(150, 100)">
<!-- Main rectangle -->
<rect x="0" y="0" width="100" height="120" rx="8"
fill="none"
stroke="currentColor"
stroke-width="3"
opacity="0.3"/>
<!-- Lines representing text -->
<line x1="15" y1="25" x2="85" y2="25"
stroke="currentColor"
stroke-width="2"
opacity="0.2"/>
<line x1="15" y1="45" x2="70" y2="45"
stroke="currentColor"
stroke-width="2"
opacity="0.2"/>
<line x1="15" y1="65" x2="85" y2="65"
stroke="currentColor"
stroke-width="2"
opacity="0.2"/>
<line x1="15" y1="85" x2="60" y2="85"
stroke="currentColor"
stroke-width="2"
opacity="0.2"/>
</g>
<!-- Plus icon overlay -->
<g transform="translate(200, 150)">
<circle cx="35" cy="35" r="25" fill="#3B82F6"/>
<line x1="35" y1="25" x2="35" y2="45" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round"/>
<line x1="25" y1="35" x2="45" y2="35" stroke="#FFFFFF" stroke-width="3" stroke-linecap="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,33 @@
<svg width="400" height="300" viewBox="0 0 400 300" fill="none" xmlns="http://www.w3.org/2000/svg">
<!--
EN: Error State Illustration - Alert triangle with brand styling
VI: Minh họa Error State - Tam giác cảnh báo với style thương hiệu
-->
<!-- Background circle with error color -->
<circle cx="200" cy="150" r="120"
fill="#EF4444"
opacity="0.1"/>
<!-- Alert triangle -->
<g transform="translate(200, 150)">
<!-- Triangle outline -->
<path d="M 0,-70 L 60,50 L -60,50 Z"
fill="none"
stroke="#EF4444"
stroke-width="4"
stroke-linejoin="round"/>
<!-- Exclamation mark -->
<line x1="0" y1="-30" x2="0" y2="10"
stroke="#EF4444"
stroke-width="6"
stroke-linecap="round"/>
<circle cx="0" cy="30" r="4" fill="#EF4444"/>
</g>
<!-- Decorative elements -->
<circle cx="120" cy="80" r="8" fill="#EF4444" opacity="0.2"/>
<circle cx="280" cy="220" r="12" fill="#EF4444" opacity="0.2"/>
<circle cx="290" cy="90" r="6" fill="#EF4444" opacity="0.2"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,54 @@
<svg width="280" height="48" viewBox="0 0 280 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<!--
EN: GoodGo Platform Full Logo - Icon + Wordmark
VI: Logo đầy đủ GoodGo Platform - Icon + Wordmark
-->
<defs>
<linearGradient id="iconGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3B82F6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#8B5CF6;stop-opacity:1" />
</linearGradient>
<linearGradient id="textGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#3B82F6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#8B5CF6;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Icon (scaled to 48x48) -->
<g transform="translate(0, 0)">
<!-- Main hexagon -->
<path d="M24 6 L37.5 15 L37.5 33 L24 42 L10.5 33 L10.5 15 Z"
fill="url(#iconGradient)"
opacity="0.9"/>
<!-- Inner hexagon -->
<path d="M24 13.5 L31.5 18.75 L31.5 29.25 L24 34.5 L16.5 29.25 L16.5 18.75 Z"
fill="#FFFFFF"
opacity="0.2"/>
<!-- Center dot -->
<circle cx="24" cy="24" r="3" fill="#FFFFFF"/>
</g>
<!-- Wordmark -->
<g transform="translate(60, 0)">
<!-- GoodGo text -->
<text x="0" y="32"
font-family="Inter, sans-serif"
font-size="28"
font-weight="700"
fill="url(#textGradient)">
GoodGo
</text>
<!-- Platform text -->
<text x="115" y="32"
font-family="Inter, sans-serif"
font-size="18"
font-weight="400"
fill="currentColor"
opacity="0.6">
Platform
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -0,0 +1,28 @@
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<!--
EN: GoodGo Platform Logo Icon - Geometric minimalist design
VI: Logo Icon GoodGo Platform - Thiết kế hình học tối giản
Design concept: Overlapping hexagons representing microservices architecture
Màu sắc: Brand gradient (Blue to Purple)
-->
<defs>
<linearGradient id="brandGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#3B82F6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#8B5CF6;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Main hexagon -->
<path d="M32 8 L50 20 L50 44 L32 56 L14 44 L14 20 Z"
fill="url(#brandGradient)"
opacity="0.9"/>
<!-- Inner hexagon -->
<path d="M32 18 L42 25 L42 39 L32 46 L22 39 L22 25 Z"
fill="#FFFFFF"
opacity="0.2"/>
<!-- Center dot -->
<circle cx="32" cy="32" r="4" fill="#FFFFFF"/>
</svg>

After

Width:  |  Height:  |  Size: 996 B

View File

@@ -0,0 +1,31 @@
<svg width="200" height="40" viewBox="0 0 200 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<!--
EN: GoodGo Platform Wordmark - Clean typography
VI: Wordmark GoodGo Platform - Typography sạch sẽ
-->
<defs>
<linearGradient id="textGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#3B82F6;stop-opacity:1" />
<stop offset="100%" style="stop-color:#8B5CF6;stop-opacity:1" />
</linearGradient>
</defs>
<!-- GoodGo text -->
<text x="0" y="28"
font-family="Inter, sans-serif"
font-size="24"
font-weight="700"
fill="url(#textGradient)">
GoodGo
</text>
<!-- Platform text -->
<text x="100" y="28"
font-family="Inter, sans-serif"
font-size="16"
font-weight="400"
fill="currentColor"
opacity="0.6">
Platform
</text>
</svg>

After

Width:  |  Height:  |  Size: 896 B

View File

@@ -0,0 +1,23 @@
{
"name": "GoodGo Platform",
"short_name": "GoodGo",
"description": "Enterprise Microservices Platform - Build, deploy, and scale with confidence",
"start_url": "/",
"display": "standalone",
"background_color": "#000000",
"theme_color": "#3B82F6",
"orientation": "portrait-primary",
"icons": [
{
"src": "/brand-assets/icons/favicon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any maskable"
}
],
"categories": [
"developer tools",
"productivity"
],
"lang": "en-US"
}

View File

@@ -31,7 +31,7 @@
font-size: var(--text-base);
line-height: 1.5;
transition: background-color var(--duration-normal) var(--ease-in-out),
color var(--duration-normal) var(--ease-in-out);
color var(--duration-normal) var(--ease-in-out);
}
/**
@@ -42,8 +42,8 @@
*::before,
*::after {
transition: background-color var(--duration-normal) var(--ease-in-out),
color var(--duration-normal) var(--ease-in-out),
border-color var(--duration-normal) var(--ease-in-out);
color var(--duration-normal) var(--ease-in-out),
border-color var(--duration-normal) var(--ease-in-out);
}
/**
@@ -122,10 +122,14 @@
* VI: Animation cho Typing Indicator
*/
@keyframes typing-pulse {
0%, 60%, 100% {
0%,
60%,
100% {
opacity: 0.3;
transform: scale(0.8);
}
30% {
opacity: 1;
transform: scale(1);
@@ -145,12 +149,27 @@
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/**
* EN: Shimmer animation for skeleton loaders
* VI: Animation shimmer cho skeleton loaders
*/
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
/**
* EN: Ensure smooth animations and prevent layout shift
* VI: Đảm bảo animation mượt mà và ngăn layout shift
@@ -159,4 +178,8 @@
.animate-fadeIn {
animation: fadeIn 0.3s ease-out forwards;
}
}
.animate-shimmer {
animation: shimmer 2s infinite;
}
}

View File

@@ -10,8 +10,45 @@ import { SkipToContent } from '../components/accessibility/skip-to-content';
* VI: Metadata cho ứng dụng
*/
export const metadata: Metadata = {
title: 'GoodGo Platform',
description: 'Enterprise microservices platform / Nền tảng microservices doanh nghiệp',
title: {
default: 'GoodGo Platform - Enterprise Microservices',
template: '%s | GoodGo Platform',
},
description: 'Build, deploy, and scale microservices with confidence. Enterprise-grade microservices platform for modern development teams.',
keywords: ['microservices', 'enterprise', 'platform', 'cloud', 'kubernetes', 'devops'],
// EN: Brand icons for all platforms / VI: Brand icons cho tất cả platforms
icons: {
icon: '/brand-assets/icons/favicon.svg',
apple: '/brand-assets/icons/favicon.svg',
},
// EN: Open Graph for social media sharing / VI: Open Graph cho chia sẻ mạng xã hội
openGraph: {
type: 'website',
locale: 'en_US',
url: 'https://goodgo.com',
siteName: 'GoodGo Platform',
title: 'GoodGo Platform - Enterprise Microservices',
description: 'Build, deploy, and scale microservices with confidence',
},
// EN: Twitter Card metadata / VI: Twitter Card metadata
twitter: {
card: 'summary_large_image',
title: 'GoodGo Platform',
description: 'Enterprise Microservices Platform',
},
// EN: PWA manifest / VI: PWA manifest
manifest: '/manifest.json',
// EN: Viewport configuration / VI: Cấu hình viewport
viewport: {
width: 'device-width',
initialScale: 1,
maximumScale: 5,
},
};
/**

View File

@@ -3,10 +3,12 @@
import { useAuthStore } from '@/stores/auth-store';
import { useEffect, useState } from 'react';
import { useTranslation } from '@/hooks/use-translation';
import { BrandLogo } from '@/components/ui/brand-logo';
import { Button } from '@/components/ui/button';
/**
* EN: Home page component - main application entry point
* VI: Component trang chủ - điểm vào chính của ứng dụng
* EN: Home page component - main application entry point with brand elements
* VI: Component trang chủ - điểm vào chính với brand elements
*/
export default function Home() {
// EN: Translation hook / VI: Hook translation
@@ -46,21 +48,38 @@ export default function Home() {
// EN: Show loading state while checking authentication
// VI: Hiển thị trạng thái loading trong khi kiểm tra xác thực
if (isLoading) {
return <div className="min-h-screen flex items-center justify-center p-8 bg-bg-primary text-text-primary">{t('common.loading')}</div>;
return (
<div className="min-h-screen flex items-center justify-center p-8 bg-bg-primary text-text-primary">
{t('common.loading')}
</div>
);
}
return (
// EN: Main content area with centered layout for minimal aesthetic
// VI: Khu vực nội dung chính với layout căn giữa cho thẩm mỹ tối giản
<main className="min-h-screen flex flex-col items-center justify-center p-8 text-center bg-bg-primary text-text-primary">
<div className="max-w-container-md w-full space-y-8 animate-fadeIn">
// EN: Main content area with brand gradient background
// VI: Khu vực nội dung chính với background gradient thương hiệu
<main className="relative min-h-screen flex flex-col items-center justify-center p-8 text-center bg-bg-primary text-text-primary overflow-hidden">
{/* EN: Brand gradient overlay / VI: Overlay gradient thương hiệu */}
<div className="absolute inset-0 bg-brand-gradient opacity-5 -z-10" />
{/* EN: Decorative brand dots / VI: Chấm trang trí thương hiệu */}
<div className="absolute top-20 left-10 w-3 h-3 rounded-full bg-brand-primary opacity-20 animate-pulse" />
<div className="absolute top-40 right-20 w-4 h-4 rounded-full bg-brand-secondary opacity-20 animate-pulse" style={{ animationDelay: '1s' }} />
<div className="absolute bottom-32 left-1/4 w-2 h-2 rounded-full bg-brand-accent opacity-20 animate-pulse" style={{ animationDelay: '2s' }} />
<div className="max-w-container-lg w-full space-y-12 animate-fadeIn z-10">
{/* EN: Brand Logo / VI: Logo thương hiệu */}
<div className="flex justify-center mb-8">
<BrandLogo variant="full" size="xl" priority />
</div>
{/* EN: Hero Title / VI: Tiêu đề Hero */}
<h1 className="text-6xl font-bold tracking-tight mb-2 font-display">
<h1 className="text-6xl font-display font-bold tracking-tight mb-4 bg-clip-text text-transparent bg-brand-gradient-vertical leading-tight">
{t('home.title')}
</h1>
{/* EN: Subtitle/Description / VI: Phụ đề/Mô tả */}
<p className="text-xl text-text-secondary max-w-lg mx-auto leading-relaxed">
<p className="text-xl text-text-secondary max-w-2xl mx-auto leading-relaxed">
{t('home.description')}
</p>
@@ -68,31 +87,69 @@ export default function Home() {
<div className="pt-8">
{isAuthenticated && user ? (
// EN: Authenticated user welcome message / VI: Thông báo chào mừng người dùng đã xác thực
<div className="space-y-4 p-6 rounded-2xl bg-bg-secondary border border-border-primary inline-block min-w-[300px]">
<div className="w-16 h-16 bg-bg-tertiary rounded-full mx-auto flex items-center justify-center mb-4 text-2xl">
<div className="space-y-6 p-8 rounded-2xl bg-glass-bg backdrop-blur-glass border border-glass-border inline-block min-w-[320px] shadow-brand">
<div className="w-20 h-20 bg-brand-gradient rounded-full mx-auto flex items-center justify-center mb-6 text-3xl font-semibold text-white shadow-colored">
{user.email?.[0].toUpperCase()}
</div>
<p className="text-lg font-medium">{t('home.welcome', { email: user.email })}</p>
<div className="inline-flex items-center px-3 py-1 rounded-full bg-bg-tertiary text-text-secondary text-sm">
<span className="w-2 h-2 rounded-full bg-accent-success mr-2"></span>
<p className="text-lg font-medium text-text-primary">
{t('home.welcome', { email: user.email })}
</p>
<div className="inline-flex items-center px-4 py-2 rounded-full bg-bg-tertiary text-text-secondary text-sm border border-border-primary">
<span className="w-2 h-2 rounded-full bg-accent-success mr-2 animate-pulse" />
{t('home.role', { role: user.role })}
</div>
</div>
) : (
// EN: Login prompt for unauthenticated users / VI: Nhắc đăng nhập cho người dùng chưa xác thực
<div className="space-y-6">
<p className="text-text-secondary">{t('home.pleaseLogin')}</p>
<div className="flex gap-4 justify-center">
<a
href="/auth/login"
className="px-8 py-3 rounded-full bg-text-primary text-bg-primary font-medium hover:bg-text-secondary transition-colors"
<div className="space-y-8">
<p className="text-text-secondary text-lg">
{t('home.pleaseLogin')}
</p>
<div className="flex gap-4 justify-center flex-wrap">
<Button
variant="brand"
size="xl"
onClick={() => (window.location.href = '/auth/login')}
className="px-12"
>
Login
</a>
Get Started
</Button>
<Button
variant="glass"
size="xl"
onClick={() => (window.location.href = '/auth/login')}
className="px-12"
>
Learn More
</Button>
</div>
</div>
)}
</div>
{/* EN: Feature highlights / VI: Điểm nổi bật tính năng */}
{!isAuthenticated && (
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 pt-16 max-w-4xl mx-auto">
{[
{ title: 'Fast Development', icon: '⚡', desc: 'Build and deploy in minutes' },
{ title: 'Enterprise Ready', icon: '🏢', desc: 'Production-grade platform' },
{ title: 'Developer First', icon: '💻', desc: 'Built for modern teams' },
].map((feature, index) => (
<div
key={index}
className="p-6 rounded-xl bg-glass-bg backdrop-blur-glass border border-glass-border hover:border-brand-primary/30 transition-all duration-normal hover:shadow-brand group"
>
<div className="text-4xl mb-4 group-hover:scale-110 transition-transform duration-normal">
{feature.icon}
</div>
<h3 className="text-lg font-semibold mb-2 text-text-primary">
{feature.title}
</h3>
<p className="text-sm text-text-secondary">{feature.desc}</p>
</div>
))}
</div>
)}
</div>
</main>
);

View File

@@ -0,0 +1,140 @@
'use client';
import React from 'react';
import Image from 'next/image';
import { useTheme } from '@/contexts/theme-context';
import { cn } from '@/lib/utils';
import { BRAND, getBrandLogo } from '@/lib/brand-constants';
/**
* EN: Brand logo variant types
* VI: Các kiểu biến thể logo thương hiệu
*/
export type LogoVariant = 'full' | 'icon' | 'wordmark';
/**
* EN: Brand logo size presets
* VI: Kích thước logo định sẵn
*/
export type LogoSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
/**
* EN: Brand logo component props
* VI: Props cho component logo thương hiệu
*/
export interface BrandLogoProps {
/** Logo variant / Biến thể logo */
variant?: LogoVariant;
/** Logo size / Kích thước logo */
size?: LogoSize;
/** Additional CSS classes / CSS classes bổ sung */
className?: string;
/** Whether to use Next.js Image optimization / Có sử dụng tối ưu Image của Next.js không */
priority?: boolean;
}
/**
* EN: Size mappings for different logo sizes
* VI: Ánh xạ kích thước cho các size logo khác nhau
*/
const sizeClasses: Record<LogoSize, { width: string; height: string }> = {
xs: { width: 'w-16', height: 'h-4' },
sm: { width: 'w-24', height: 'h-6' },
md: { width: 'w-32', height: 'h-8' },
lg: { width: 'w-48', height: 'h-12' },
xl: { width: 'w-64', height: 'h-16' },
'2xl': { width: 'w-80', height: 'h-20' },
};
/**
* EN: Size mappings for icon variant
* VI: Ánh xạ kích thước cho biến thể icon
*/
const iconSizeClasses: Record<LogoSize, { width: string; height: string }> = {
xs: { width: 'w-4', height: 'h-4' },
sm: { width: 'w-6', height: 'h-6' },
md: { width: 'w-8', height: 'h-8' },
lg: { width: 'w-12', height: 'h-12' },
xl: { width: 'w-16', height: 'h-16' },
'2xl': { width: 'w-20', height: 'h-20' },
};
/**
* EN: Brand Logo Component - Displays brand logo with automatic theme switching
* VI: Component Logo Thương Hiệu - Hiển thị logo với tự động chuyển theme
*
* @example
* ```tsx
* <BrandLogo variant="full" size="lg" />
* <BrandLogo variant="icon" size="md" />
* <BrandLogo variant="wordmark" size="sm" />
* ```
*/
export function BrandLogo({
variant = 'full',
size = 'md',
className,
priority = false,
}: BrandLogoProps) {
const { resolvedTheme } = useTheme();
// EN: Get logo path based on variant
// VI: Lấy đường dẫn logo dựa trên biến thể
const logoSrc = getBrandLogo(variant);
// EN: Select size classes based on variant
// VI: Chọn class kích thước dựa trên biến thể
const sizes = variant === 'icon' ? iconSizeClasses[size] : sizeClasses[size];
return (
<div
className={cn(
'relative flex items-center justify-center',
sizes.width,
sizes.height,
className
)}
>
<Image
src={logoSrc}
alt={BRAND.name}
fill
priority={priority}
className="object-contain"
style={{
// EN: Apply theme-aware color for SVG text elements
// VI: Áp dụng màu nhận biết theme cho SVG text elements
color: resolvedTheme === 'dark' ? '#FFFFFF' : '#000000',
}}
/>
</div>
);
}
/**
* EN: Brand Logo Link - Clickable logo that links to home
* VI: Logo Link - Logo có thể nhấp dẫn về trang chủ
*
* @example
* ```tsx
* <BrandLogoLink variant="full" size="lg" href="/" />
* ```
*/
export interface BrandLogoLinkProps extends BrandLogoProps {
href?: string;
}
export function BrandLogoLink({
href = '/',
...logoProps
}: BrandLogoLinkProps) {
return (
<a
href={href}
className="inline-block transition-opacity hover:opacity-80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-primary focus-visible:ring-offset-2 rounded-sm"
aria-label={`${BRAND.name} - Go to homepage`}
>
<BrandLogo {...logoProps} />
</a>
);
}

View File

@@ -24,6 +24,12 @@ const buttonVariants = cva(
// EN: Danger button - destructive actions / VI: Button nguy hiểm - hành động phá hủy
danger:
'bg-accent-error text-white hover:brightness-110 hover:scale-[1.02] active:scale-[0.98] active:brightness-90 focus-visible:ring-accent-error focus-visible:shadow-[0_0_20px_rgba(239,68,68,0.3)] shadow-md hover:shadow-lg',
// EN: Brand button - main CTA with brand gradient / VI: Button thương hiệu - CTA chính với brand gradient
brand:
'bg-brand-gradient text-white hover:shadow-brand-lg hover:scale-[1.02] active:scale-[0.98] focus-visible:ring-brand-primary focus-visible:shadow-colored shadow-brand transition-all duration-fast',
// EN: Glass button - glassmorphism style / VI: Button glass - phong cách glassmorphism
glass:
'bg-glass-bg backdrop-blur-glass border border-glass-border text-white hover:bg-glass-bg-hover hover:border-white/20 hover:scale-[1.02] active:scale-[0.98] focus-visible:ring-brand-primary shadow-md',
},
size: {
// EN: Extra small button - 28px height (mobile: min 44px) / VI: Button cực nhỏ - chiều cao 28px (mobile: tối thiểu 44px)
@@ -51,7 +57,7 @@ const buttonVariants = cva(
*/
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
VariantProps<typeof buttonVariants> {
/**
* EN: Loading state - shows spinner when true / VI: Trạng thái loading - hiển thị spinner khi true
*/

View File

@@ -0,0 +1,153 @@
'use client';
import React from 'react';
import Image from 'next/image';
import { cn } from '@/lib/utils';
import { getBrandIllustration } from '@/lib/brand-constants';
import { Button } from './button';
/**
* EN: Empty state action button configuration
* VI: Cấu hình button hành động cho empty state
*/
export interface EmptyStateAction {
/** Button label / Nhãn button */
label: string;
/** Click handler / Hàm xử lý click */
onClick: () => void;
/** Button variant / Biến thể button */
variant?: 'brand' | 'primary' | 'secondary' | 'ghost';
}
/**
* EN: Empty state component props
* VI: Props cho component empty state
*/
export interface EmptyStateProps {
/** Title text / Tiêu đề */
title: string;
/** Description text / Mô tả */
description?: string;
/** Illustration type / Loại minh họa */
illustration?: 'empty' | 'error' | 'custom';
/** Custom illustration path / Đường dẫn minh họa tùy chỉnh */
customIllustration?: string;
/** Primary action button / Button hành động chính */
action?: EmptyStateAction;
/** Secondary action button / Button hành động phụ */
secondaryAction?: EmptyStateAction;
/** Additional CSS classes / CSS classes bổ sung */
className?: string;
/** Show illustration / Hiển thị minh họa */
showIllustration?: boolean;
}
/**
* EN: Empty State Component - Professional empty state with illustrations
* VI: Component Empty State - Empty state chuyên nghiệp với minh họa
*
* @example
* ```tsx
* <EmptyState
* title="No data found"
* description="Try adding some items to get started"
* illustration="empty"
* action={{ label: 'Add Item', onClick: () => {} }}
* />
* ```
*/
export function EmptyState({
title,
description,
illustration = 'empty',
customIllustration,
action,
secondaryAction,
className,
showIllustration = true,
}: EmptyStateProps) {
// EN: Get illustration path
// VI: Lấy đường dẫn minh họa
const illustrationSrc =
illustration === 'custom' && customIllustration
? customIllustration
: getBrandIllustration(illustration === 'custom' ? 'empty' : illustration);
return (
<div
className={cn(
'flex flex-col items-center justify-center p-12 text-center min-h-[400px]',
className
)}
role="status"
aria-live="polite"
>
{/* EN: Illustration / VI: Minh họa */}
{showIllustration && (
<div className="relative w-64 h-64 mb-8 opacity-60 hover:opacity-80 transition-opacity duration-normal">
<Image
src={illustrationSrc}
alt={title}
fill
className="object-contain"
priority={false}
/>
</div>
)}
{/* EN: Title / VI: Tiêu đề */}
<h3 className="text-2xl font-heading font-semibold mb-3 text-text-primary">
{title}
</h3>
{/* EN: Description / VI: Mô tả */}
{description && (
<p className="text-text-secondary mb-8 max-w-md leading-relaxed">
{description}
</p>
)}
{/* EN: Actions / VI: Hành động */}
{(action || secondaryAction) && (
<div className="flex gap-4 items-center">
{action && (
<Button
variant={action.variant || 'brand'}
size="lg"
onClick={action.onClick}
className="shadow-brand hover:shadow-brand-lg"
>
{action.label}
</Button>
)}
{secondaryAction && (
<Button
variant={secondaryAction.variant || 'ghost'}
size="lg"
onClick={secondaryAction.onClick}
>
{secondaryAction.label}
</Button>
)}
</div>
)}
</div>
);
}
/**
* EN: Error State Component - Specialized empty state for errors
* VI: Component Error State - Empty state chuyên biệt cho lỗi
*
* @example
* ```tsx
* <ErrorState
* title="Something went wrong"
* description="Please try again or contact support"
* action={{ label: 'Try Again', onClick: () => {} }}
* />
* ```
*/
export function ErrorState(props: Omit<EmptyStateProps, 'illustration'>) {
return <EmptyState {...props} illustration="error" />;
}

View File

@@ -0,0 +1,269 @@
'use client';
import React from 'react';
import { cn } from '@/lib/utils';
/**
* EN: Loading spinner size types
* VI: Kiểu kích thước loading spinner
*/
export type SpinnerSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
/**
* EN: Loading spinner props
* VI: Props cho loading spinner
*/
export interface SpinnerProps {
/** Spinner size / Kích thước spinner */
size?: SpinnerSize;
/** Spinner color / Màu spinner */
color?: 'brand' | 'primary' | 'white';
/** Additional CSS classes / CSS classes bổ sung */
className?: string;
}
/**
* EN: Size mappings for spinner
* VI: Ánh xạ kích thước cho spinner
*/
const spinnerSizes: Record<SpinnerSize, string> = {
xs: 'w-4 h-4 border-2',
sm: 'w-6 h-6 border-2',
md: 'w-8 h-8 border-3',
lg: 'w-12 h-12 border-4',
xl: 'w-16 h-16 border-4',
};
/**
* EN: Color mappings for spinner
* VI: Ánh xạ màu cho spinner
*/
const spinnerColors: Record<string, string> = {
brand: 'border-brand-primary/20 border-t-brand-primary',
primary: 'border-text-primary/20 border-t-text-primary',
white: 'border-white/20 border-t-white',
};
/**
* EN: Brand Spinner - Loading spinner with brand colors
* VI: Brand Spinner - Loading spinner với màu thương hiệu
*
* @example
* ```tsx
* <BrandSpinner size="md" color="brand" />
* ```
*/
export function BrandSpinner({
size = 'md',
color = 'brand',
className,
}: SpinnerProps) {
return (
<div
className={cn(
'inline-block rounded-full animate-spin',
spinnerSizes[size],
spinnerColors[color],
className
)}
role="status"
aria-label="Loading"
>
<span className="sr-only">Loading...</span>
</div>
);
}
/**
* EN: Skeleton component props
* VI: Props cho skeleton component
*/
export interface SkeletonProps {
/** Skeleton variant / Biến thể skeleton */
variant?: 'text' | 'circular' | 'rectangular';
/** Width / Chiều rộng */
width?: string | number;
/** Height / Chiều cao */
height?: string | number;
/** Additional CSS classes / CSS classes bổ sung */
className?: string;
/** Animation wave effect / Hiệu ứng sóng animation */
wave?: boolean;
}
/**
* EN: Skeleton - Loading placeholder with shimmer effect
* VI: Skeleton - Loading placeholder với hiệu ứng shimmer
*
* @example
* ```tsx
* <Skeleton variant="text" width="100%" height="20px" />
* <Skeleton variant="circular" width="40px" height="40px" />
* <Skeleton variant="rectangular" width="200px" height="100px" />
* ```
*/
export function Skeleton({
variant = 'rectangular',
width,
height,
className,
wave = true,
}: SkeletonProps) {
const variantClasses = {
text: 'rounded',
circular: 'rounded-full',
rectangular: 'rounded-md',
};
return (
<div
className={cn(
'bg-bg-tertiary animate-pulse',
variantClasses[variant],
wave && 'relative overflow-hidden',
className
)}
style={{
width: typeof width === 'number' ? `${width}px` : width,
height: typeof height === 'number' ? `${height}px` : height,
}}
role="status"
aria-label="Loading"
>
{wave && (
<div className="absolute inset-0 -translate-x-full animate-shimmer bg-gradient-to-r from-transparent via-white/10 to-transparent" />
)}
<span className="sr-only">Loading...</span>
</div>
);
}
/**
* EN: Skeleton Card - Pre-built skeleton for card layouts
* VI: Skeleton Card - Skeleton định sẵn cho layout card
*/
export function SkeletonCard({ className }: { className?: string }) {
return (
<div className={cn('p-6 border border-border-primary rounded-lg', className)}>
<Skeleton variant="circular" width="48px" height="48px" className="mb-4" />
<Skeleton variant="text" width="70%" height="24px" className="mb-2" />
<Skeleton variant="text" width="100%" height="16px" className="mb-1" />
<Skeleton variant="text" width="90%" height="16px" className="mb-4" />
<div className="flex gap-2">
<Skeleton variant="rectangular" width="80px" height="32px" />
<Skeleton variant="rectangular" width="80px" height="32px" />
</div>
</div>
);
}
/**
* EN: Skeleton List - Pre-built skeleton for list layouts
* VI: Skeleton List - Skeleton định sẵn cho layout list
*/
export function SkeletonList({
count = 3,
className,
}: {
count?: number;
className?: string;
}) {
return (
<div className={cn('space-y-3', className)}>
{Array.from({ length: count }).map((_, index) => (
<div key={index} className="flex items-center gap-4 p-4 border border-border-primary rounded-lg">
<Skeleton variant="circular" width="40px" height="40px" />
<div className="flex-1 space-y-2">
<Skeleton variant="text" width="40%" height="16px" />
<Skeleton variant="text" width="60%" height="14px" />
</div>
</div>
))}
</div>
);
}
/**
* EN: Loading Overlay - Full screen loading overlay
* VI: Loading Overlay - Overlay loading toàn màn hình
*/
export interface LoadingOverlayProps {
/** Loading message / Thông báo loading */
message?: string;
/** Show overlay / Hiển thị overlay */
show: boolean;
/** Spinner size / Kích thước spinner */
size?: SpinnerSize;
}
export function LoadingOverlay({
message = 'Loading...',
show,
size = 'lg',
}: LoadingOverlayProps) {
if (!show) return null;
return (
<div
className="fixed inset-0 z-50 flex flex-col items-center justify-center bg-bg-primary/80 backdrop-blur-sm"
role="status"
aria-live="assertive"
aria-label={message}
>
<BrandSpinner size={size} color="brand" />
<p className="mt-6 text-lg text-text-secondary font-medium">{message}</p>
</div>
);
}
/**
* EN: Progress Bar - Linear progress indicator with brand colors
* VI: Progress Bar - Thanh tiến trình với màu thương hiệu
*/
export interface ProgressBarProps {
/** Progress value (0-100) / Giá trị tiến trình (0-100) */
value: number;
/** Show percentage label / Hiển thị nhãn phần trăm */
showLabel?: boolean;
/** Additional CSS classes / CSS classes bổ sung */
className?: string;
/** Progress bar color / Màu thanh tiến trình */
color?: 'brand' | 'success' | 'warning' | 'error';
}
const progressColors: Record<string, string> = {
brand: 'bg-brand-gradient',
success: 'bg-accent-success',
warning: 'bg-accent-warning',
error: 'bg-accent-error',
};
export function ProgressBar({
value,
showLabel = false,
className,
color = 'brand',
}: ProgressBarProps) {
const clampedValue = Math.max(0, Math.min(100, value));
return (
<div className={cn('w-full', className)} role="progressbar" aria-valuenow={clampedValue} aria-valuemin={0} aria-valuemax={100}>
<div className="flex items-center gap-3">
<div className="flex-1 h-2 bg-bg-tertiary rounded-full overflow-hidden">
<div
className={cn(
'h-full transition-all duration-slow ease-out',
progressColors[color]
)}
style={{ width: `${clampedValue}%` }}
/>
</div>
{showLabel && (
<span className="text-sm font-medium text-text-secondary min-w-[3ch] text-right">
{Math.round(clampedValue)}%
</span>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1,122 @@
/**
* EN: Brand Constants - Easy access to brand values
* VI: Hằng số thương hiệu - Truy cập dễ dàng các giá trị thương hiệu
*
* This file provides type-safe constants for brand colors, fonts, and assets.
* Use these instead of hardcoding values for consistency and maintainability.
*
* File này cung cấp constants type-safe cho màu sắc, fonts và assets thương hiệu.
* Sử dụng các constants này thay vì hardcode giá trị để đảm bảo tính nhất quán và dễ bảo trì.
*/
export const BRAND = {
/** EN: Brand name and tagline / VI: Tên thương hiệu và slogan */
name: 'GoodGo Platform',
tagline: 'Enterprise Microservices Platform',
description: 'Build, deploy, and scale microservices with confidence',
/**
* EN: Brand color palette - Use these for consistent branding
* VI: Bảng màu thương hiệu - Sử dụng để đảm bảo tính nhất quán
*/
colors: {
/** Primary brand color - Main identity color (Blue) */
primary: {
main: 'var(--brand-primary)',
light: 'var(--brand-primary-light)',
dark: 'var(--brand-primary-dark)',
contrast: 'var(--brand-primary-contrast)',
hex: '#3B82F6', // For use outside CSS
},
/** Secondary brand color - Supporting color (Purple) */
secondary: {
main: 'var(--brand-secondary)',
light: 'var(--brand-secondary-light)',
dark: 'var(--brand-secondary-dark)',
hex: '#8B5CF6',
},
/** Accent color - Call-to-action color (Cyan) */
accent: {
main: 'var(--brand-accent)',
hex: '#06B6D4',
},
/** Brand gradients - For backgrounds and special elements */
gradients: {
primary: 'var(--brand-gradient-primary)',
accent: 'var(--brand-gradient-accent)',
},
},
/**
* EN: Typography system - Font families for different use cases
* VI: Hệ thống typography - Font families cho các trường hợp khác nhau
*/
fonts: {
display: 'var(--font-display)', // For hero titles (48px+)
heading: 'var(--font-heading)', // For section headings (24-36px)
body: 'var(--font-sans)', // For body text (16px)
mono: 'var(--font-mono)', // For code blocks
},
/**
* EN: Brand assets paths - Logo, icons, illustrations
* VI: Đường dẫn brand assets - Logo, icons, illustrations
*/
assets: {
logo: {
full: '/brand-assets/logo/logo-full.svg',
icon: '/brand-assets/logo/logo-icon.svg',
wordmark: '/brand-assets/logo/logo-wordmark.svg',
},
icons: {
favicon: '/brand-assets/icons/favicon.svg',
},
illustrations: {
empty: '/brand-assets/illustrations/empty-state.svg',
error: '/brand-assets/illustrations/error-state.svg',
},
},
} as const;
/**
* EN: Type-safe brand color getter
* VI: Hàm lấy màu thương hiệu type-safe
*
* @example
* ```tsx
* const primaryColor = getBrandColor('primary.main');
* const secondaryHex = getBrandColor('secondary.hex');
* ```
*/
export const getBrandColor = (path: string): string => {
const keys = path.split('.');
let value: any = BRAND.colors;
for (const key of keys) {
value = value?.[key];
}
return value || '';
};
/**
* EN: Brand logo path getter
* VI: Hàm lấy đường dẫn logo thương hiệu
*
* @param variant - Logo variant: 'full' | 'icon' | 'wordmark'
* @returns Logo file path
*/
export const getBrandLogo = (variant: 'full' | 'icon' | 'wordmark' = 'full'): string => {
return BRAND.assets.logo[variant];
};
/**
* EN: Brand illustration path getter
* VI: Hàm lấy đường dẫn illustration thương hiệu
*
* @param type - Illustration type: 'empty' | 'error'
* @returns Illustration file path
*/
export const getBrandIllustration = (type: 'empty' | 'error'): string => {
return BRAND.assets.illustrations[type];
};

View File

@@ -75,6 +75,51 @@
--border-focus: #FFFFFF;
/* Focus state - White */
/* ============================================
EN: Brand Colors (Primary Identity)
VI: Màu thương hiệu (Nhận diện chính)
============================================ */
/* Primary Brand Color - Main brand identity (Blue - Tech & Trust) */
--brand-primary: #3B82F6;
--brand-primary-light: #60A5FA;
--brand-primary-dark: #2563EB;
--brand-primary-contrast: #FFFFFF;
/* Secondary Brand Color - Supporting color (Purple - Innovation) */
--brand-secondary: #8B5CF6;
--brand-secondary-light: #A78BFA;
--brand-secondary-dark: #7C3AED;
/* Accent Color - Call-to-action (Cyan - Energy) */
--brand-accent: #06B6D4;
--brand-accent-light: #22D3EE;
--brand-accent-dark: #0891B2;
/* Brand Gradients - For backgrounds and special elements */
--brand-gradient-primary: linear-gradient(135deg, var(--brand-primary) 0%, var(--brand-secondary) 100%);
--brand-gradient-accent: linear-gradient(135deg, var(--brand-accent) 0%, var(--brand-primary) 100%);
--brand-gradient-vertical: linear-gradient(180deg, var(--brand-primary) 0%, var(--brand-secondary) 100%);
/* ============================================
EN: Glassmorphism Effects
VI: Hiệu ứng Glassmorphism
============================================ */
--glass-bg: rgba(255, 255, 255, 0.05);
--glass-bg-hover: rgba(255, 255, 255, 0.08);
--glass-border: rgba(255, 255, 255, 0.1);
--glass-blur: 10px;
/* ============================================
EN: Extended Shadows & Effects
VI: Shadows & Effects mở rộng
============================================ */
--shadow-brand: 0 10px 40px rgba(59, 130, 246, 0.2);
--shadow-brand-lg: 0 20px 60px rgba(59, 130, 246, 0.3);
--shadow-colored: 0 8px 30px rgba(59, 130, 246, 0.25);
/* ============================================
EN: Light Mode Colors (Secondary Theme)
VI: Màu sắc cho Light Mode (Theme phụ)
@@ -95,6 +140,12 @@
--font-sans: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
--font-mono: "JetBrains Mono", "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
/* Display Font - For hero titles (48px+) */
--font-display: "Inter Display", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
/* Heading Font - For section headings (24-36px) */
--font-heading: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
/* Type Scale / Kích thước chữ */
--text-6xl: 3.75rem;
/* 60px - Hero titles */
@@ -243,6 +294,23 @@
/* Complex animations */
--duration-slower: 500ms;
/* Page transitions */
/* ============================================
EN: Advanced Motion Tokens
VI: Token chuyển động nâng cao
============================================ */
/* Motion Ease Functions - For micro-interactions */
--motion-ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
--motion-ease-elastic: cubic-bezier(0.68, -0.6, 0.32, 1.6);
/* Hover Scale - For interactive elements */
--hover-scale-sm: 1.02;
--hover-scale-md: 1.05;
--hover-scale-lg: 1.1;
/* Active Scale - For pressed states */
--active-scale: 0.98;
}
/* ============================================

View File

@@ -58,6 +58,31 @@ module.exports = {
secondary: 'var(--border-secondary)',
focus: 'var(--border-focus)',
},
// EN: Brand colors for easy access / VI: Màu thương hiệu dễ sử dụng
brand: {
primary: {
DEFAULT: 'var(--brand-primary)',
light: 'var(--brand-primary-light)',
dark: 'var(--brand-primary-dark)',
contrast: 'var(--brand-primary-contrast)',
},
secondary: {
DEFAULT: 'var(--brand-secondary)',
light: 'var(--brand-secondary-light)',
dark: 'var(--brand-secondary-dark)',
},
accent: {
DEFAULT: 'var(--brand-accent)',
light: 'var(--brand-accent-light)',
dark: 'var(--brand-accent-dark)',
},
},
// EN: Glassmorphism utilities / VI: Utilities glassmorphism
glass: {
bg: 'var(--glass-bg)',
'bg-hover': 'var(--glass-bg-hover)',
border: 'var(--glass-border)',
},
},
// EN: Font families from CSS variables
// VI: Font families từ CSS variables
@@ -120,12 +145,26 @@ module.exports = {
},
// EN: Box shadows from CSS variables
// VI: Đổ bóng từ CSS variables
// EN: Brand gradients / VI: Gradients thương hiệu
backgroundImage: {
'brand-gradient': 'var(--brand-gradient-primary)',
'brand-gradient-accent': 'var(--brand-gradient-accent)',
'brand-gradient-vertical': 'var(--brand-gradient-vertical)',
},
// EN: Extended shadows / VI: Shadows mở rộng
boxShadow: {
sm: 'var(--shadow-sm)',
md: 'var(--shadow-md)',
lg: 'var(--shadow-lg)',
xl: 'var(--shadow-xl)',
glow: 'var(--shadow-glow)',
brand: 'var(--shadow-brand)',
'brand-lg': 'var(--shadow-brand-lg)',
colored: 'var(--shadow-colored)',
},
// EN: Glassmorphism backdrop blur / VI: Backdrop blur glassmorphism
backdropBlur: {
glass: 'var(--glass-blur)',
},
// EN: Animation timing functions
// VI: Hàm thời gian animation