This commit is contained in:
Your Name committed 2026-09-29 10:13:48 +07:00
1 parent c42dd4272f
commit 730fdb49e6
8 files changed
+1611 -72

No files matched your search

+3 -1
View File
@@ -13,6 +13,7 @@ import PageReorderPage from './pages/PageReorderPage'
import CmsMockupPage from './pages/CmsMockupPage'
import RawMaterialsPage from './pages/RawMaterialsPage'
import LoginPage from './pages/LoginPage'
import InstallSeedPage from './pages/InstallSeedPage'
import ProtectedRoute from './components/ProtectedRoute'
import OrdersPage from './pages/OrdersPage'
import OrderDetailPage from './pages/OrderDetailPage'
@@ -78,8 +79,9 @@ function App() {
<StoreSettingsProvider>
<BrowserRouter>
<Routes>
{/* Public Route */}
{/* Public Routes */}
<Route path="/login" element={<LoginPage />} />
<Route path="/install-seed" element={<InstallSeedPage />} />
{/* Guarded Admin Routes */}
<Route element={<ProtectedRoute />}>
@@ -0,0 +1,869 @@
import React, { useState } from 'react'
import {
Monitor,
Smartphone,
Sparkles,
Star,
MessageCircle,
ShoppingCart,
Search,
CheckCircle2,
AlertTriangle,
ChevronDown,
ChevronUp,
ChevronLeft,
ChevronRight,
Info
} from 'lucide-react'
interface ThemeLivePreviewProps {
tokens: Record<string, string>
}
// ── WCAG Relative Luminance & Contrast Calculation ──────────────────────────
function parseHex(hex: string): [number, number, number] {
const clean = (hex || '').replace('#', '').trim()
if (clean.length === 3) {
const r = parseInt(clean[0] + clean[0], 16) || 0
const g = parseInt(clean[1] + clean[1], 16) || 0
const b = parseInt(clean[2] + clean[2], 16) || 0
return [r, g, b]
}
if (clean.length >= 6) {
const r = parseInt(clean.substring(0, 2), 16) || 0
const g = parseInt(clean.substring(2, 4), 16) || 0
const b = parseInt(clean.substring(4, 6), 16) || 0
return [r, g, b]
}
return [0, 0, 0]
}
function getLuminance(r: number, g: number, b: number): number {
const a = [r, g, b].map(v => {
v /= 255
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4)
})
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722
}
function getContrastRatio(hex1: string, hex2: string): number {
const [r1, g1, b1] = parseHex(hex1)
const [r2, g2, b2] = parseHex(hex2)
const l1 = getLuminance(r1, g1, b1)
const l2 = getLuminance(r2, g2, b2)
const brightest = Math.max(l1, l2)
const darkest = Math.min(l1, l2)
return (brightest + 0.05) / (darkest + 0.05)
}
export default function ThemeLivePreview({ tokens }: ThemeLivePreviewProps) {
const [viewMode, setViewMode] = useState<'desktop' | 'mobile'>('desktop')
const [zoomScale, setZoomScale] = useState<number>(80)
const [isExpanded, setIsExpanded] = useState(true)
const [activeTab, setActiveTab] = useState<'storefront' | 'components'>('storefront')
const [currentSlide, setCurrentSlide] = useState(0)
// Calculate Accessibility Contrast Ratios
const textOnPageContrast = getContrastRatio(tokens.textMain || '#333333', tokens.surfacePage || '#FFFFFF')
const textOnCardContrast = getContrastRatio(tokens.textMain || '#333333', tokens.surfaceCard || '#FFFFFF')
const buttonContrast = getContrastRatio('#FFFFFF', tokens.brandPrimary || '#FF6B6B')
// Sample Hero Slide Content
const heroSlides = [
{
id: 1,
tag: 'NEW COLLECTION 2026',
tagBg: 'var(--p-brand-cool)',
tagColor: 'var(--p-brand-primary)',
title: 'Pure Origins, Artisan Craft',
subtitle: 'สัมผัสกลิ่นอายความหอมที่รังสรรค์อย่างประณีต เพื่อสุนทรียภาพแห่งการผ่อนคลายในทุกมุมห้องของคุณ',
primaryBtnText: 'Shop Collection',
primaryBtnType: 'brand',
secondaryBtnText: 'Explore Scent',
secondaryBtnType: 'outline',
icon: '🕯️',
gradient: 'from-amber-500/10 via-rose-500/5 to-transparent'
},
{
id: 2,
tag: '100% NATURAL BOTANICAL',
tagBg: 'var(--p-brand-soft)',
tagColor: '#222222',
title: 'Essential Wellness & Energy',
subtitle: 'น้ำมันหอมระเหยบริสุทธิ์จากพืชพรรณธรรมชาติ ปลุกพลังความสดชื่น บำบัดอารมณ์ให้แจ่มใสในทุกวัน',
primaryBtnText: 'Order via LINE',
primaryBtnType: 'line',
secondaryBtnText: 'View Scent Guide',
secondaryBtnType: 'outline',
icon: '🌿',
gradient: 'from-emerald-500/10 via-teal-500/5 to-transparent'
},
{
id: 3,
tag: 'SPECIAL GIFT SET -30%',
tagBg: 'var(--p-brand-accent)',
tagColor: '#FFFFFF',
title: 'Luxury Sensory Experience',
subtitle: 'ชุดของขวัญอโรม่าสุดพรีเมียม ส่งมอบความรู้สึกพิเศษและบรรยากาศอันอบอุ่นแด่คนสำคัญ',
primaryBtnText: 'Claim Special Offer',
primaryBtnType: 'brand',
secondaryBtnText: 'Browse Catalog',
secondaryBtnType: 'outline',
icon: '✨',
gradient: 'from-purple-500/10 via-pink-500/5 to-transparent'
}
]
const activeSlide = heroSlides[currentSlide] || heroSlides[0]
// Inline CSS variables scope for realistic live rendering
const previewCssVars = {
'--p-brand-primary': tokens.brandPrimary || '#FF6B6B',
'--p-brand-primary-hover': tokens.brandPrimaryHover || '#FF4500',
'--p-brand-accent': tokens.brandAccent || '#FFF200',
'--p-brand-warm': tokens.brandWarm || '#FFA07A',
'--p-brand-cool': tokens.brandCool || '#E0FFFF',
'--p-brand-success': tokens.brandSuccess || '#98FB98',
'--p-brand-soft': tokens.brandSoft || '#D8BFD8',
'--p-surface-page': tokens.surfacePage || '#FFF0F5',
'--p-surface-nav-rest': tokens.surfaceNavRest || '#F9F8F5',
'--p-surface-card': tokens.surfaceCard || '#FFFFFF',
'--p-surface-muted': tokens.surfaceMuted || '#EDE6DD',
'--p-text-main': tokens.textMain || '#333333',
'--p-muted-text': tokens.mutedText || '#666666',
'--p-caption-text': tokens.captionText || '#B0A396',
'--p-border-color': tokens.borderColor || '#E8E0D8',
'--p-line-primary': tokens.linePrimary || '#00B900',
'--p-line-primary-hover': tokens.linePrimaryHover || '#009900',
'--p-radius-base': tokens.radiusBase || '8px',
'--p-radius-card': tokens.radiusCard || '16px',
'--p-shadow-card': tokens.shadowCard || '0 4px 12px rgba(0,0,0,0.08)',
'--p-shadow-card-hover': tokens.shadowCardHover || '0 12px 28px rgba(0,0,0,0.15)',
} as React.CSSProperties
return (
<div className={`admin-card border-cyan-glow/30 bg-night-950/90 shadow-2xl overflow-hidden flex flex-col transition-all duration-300 ${
isExpanded ? 'h-[calc(100vh-6.5rem)]' : 'h-auto'
}`}>
{/* ── Compact Toolbar Header ── */}
<div className="shrink-0 p-3 px-4 bg-white/[0.03] border-b border-white/5 flex flex-wrap items-center justify-between gap-2.5">
<div className="flex items-center gap-2">
<div className="p-1.5 rounded-lg bg-cyan-glow/10 text-cyan-glow">
<Sparkles className="w-4 h-4" />
</div>
<div>
<div className="flex items-center gap-1.5">
<h2 className="text-xs font-bold text-white tracking-wide">Live Preview</h2>
<span className="px-1.5 py-0.2 rounded bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 text-[9px] font-bold uppercase">
Real-Time
</span>
</div>
</div>
</div>
{/* Action Controls */}
<div className="flex items-center gap-2">
{/* Zoom / Scale Selector */}
<div className="flex items-center bg-white/5 border border-white/10 rounded-lg p-0.5 text-[10px] font-bold">
{[
{ label: '70%', val: 70 },
{ label: '80%', val: 80 },
{ label: '100%', val: 100 },
].map(z => (
<button
key={z.val}
type="button"
onClick={() => setZoomScale(z.val)}
className={`px-2 py-0.5 rounded transition cursor-pointer ${
zoomScale === z.val
? 'bg-cyan-glow text-night-950 shadow-sm'
: 'text-slate-400 hover:text-white'
}`}
title={`Scale preview to ${z.label}`}
>
{z.label}
</button>
))}
</div>
{/* View Mode Toggle */}
<div className="flex items-center p-0.5 bg-white/5 border border-white/10 rounded-lg text-xs font-semibold">
<button
type="button"
onClick={() => setViewMode('desktop')}
className={`flex items-center gap-1 px-2.5 py-1 rounded transition-all cursor-pointer ${
viewMode === 'desktop'
? 'bg-cyan-glow text-night-950 font-bold shadow-sm'
: 'text-slate-400 hover:text-white'
}`}
title="Desktop View"
>
<Monitor className="w-3 h-3" />
<span className="text-[10px]">PC</span>
</button>
<button
type="button"
onClick={() => setViewMode('mobile')}
className={`flex items-center gap-1 px-2.5 py-1 rounded transition-all cursor-pointer ${
viewMode === 'mobile'
? 'bg-cyan-glow text-night-950 font-bold shadow-sm'
: 'text-slate-400 hover:text-white'
}`}
title="Mobile View (375px)"
>
<Smartphone className="w-3 h-3" />
<span className="text-[10px]">Mobile</span>
</button>
</div>
{/* Toggle Expand/Collapse */}
<button
type="button"
onClick={() => setIsExpanded(!isExpanded)}
className="p-1 rounded-lg bg-white/5 hover:bg-white/10 text-slate-300 hover:text-white border border-white/10 transition cursor-pointer"
title={isExpanded ? 'Collapse Preview' : 'Expand Preview'}
>
{isExpanded ? <ChevronUp className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
</button>
</div>
</div>
{isExpanded && (
<>
{/* ── Sub-header: Contrast Bar & Tabs ── */}
<div className="shrink-0 px-4 py-1.5 bg-night-900/70 border-b border-white/5 flex flex-wrap items-center justify-between text-[11px] gap-2">
<div className="flex items-center gap-2">
<span className="text-[10px] text-slate-400 font-semibold uppercase">WCAG:</span>
<span className={`px-1.5 py-0.2 rounded font-mono text-[10px] font-bold ${
textOnPageContrast >= 4.5 ? 'text-emerald-400 bg-emerald-500/10' : 'text-amber-400 bg-amber-500/10'
}`}>
Page {textOnPageContrast.toFixed(1)}:1
</span>
<span className={`px-1.5 py-0.2 rounded font-mono text-[10px] font-bold ${
textOnCardContrast >= 4.5 ? 'text-emerald-400 bg-emerald-500/10' : 'text-amber-400 bg-amber-500/10'
}`}>
Card {textOnCardContrast.toFixed(1)}:1
</span>
<span className={`px-1.5 py-0.2 rounded font-mono text-[10px] font-bold ${
buttonContrast >= 4.5 ? 'text-emerald-400 bg-emerald-500/10' : 'text-amber-400 bg-amber-500/10'
}`}>
Btn {buttonContrast.toFixed(1)}:1
</span>
</div>
{/* View Tabs */}
<div className="flex items-center gap-1">
<button
type="button"
onClick={() => setActiveTab('storefront')}
className={`px-2 py-0.5 rounded text-[10px] font-bold transition cursor-pointer ${
activeTab === 'storefront' ? 'bg-cyan-glow/20 text-cyan-glow' : 'text-slate-400 hover:text-white'
}`}
>
Storefront
</button>
<button
type="button"
onClick={() => setActiveTab('components')}
className={`px-2 py-0.5 rounded text-[10px] font-bold transition cursor-pointer ${
activeTab === 'components' ? 'bg-cyan-glow/20 text-cyan-glow' : 'text-slate-400 hover:text-white'
}`}
>
Buttons
</button>
</div>
</div>
{/* ── Main Scrollable Preview Stage ── */}
<div className="flex-1 overflow-y-auto p-4 bg-night-950 flex justify-center items-start custom-scrollbar">
{/* Scoped CSS Style Sheet for Native Hover States */}
<style>{`
.preview-theme-scope {
background-color: var(--p-surface-page);
color: var(--p-text-main);
}
.preview-btn-primary {
background-color: var(--p-brand-primary);
color: #FFFFFF;
border-radius: var(--p-radius-base);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.preview-btn-primary:hover {
background-color: var(--p-brand-primary-hover);
transform: translateY(-1.5px);
box-shadow: 0 6px 16px rgba(0,0,0,0.18);
}
.preview-btn-secondary {
background-color: transparent;
color: var(--p-text-main);
border: 1px solid var(--p-border-color);
border-radius: var(--p-radius-base);
transition: all 0.2s ease;
}
.preview-btn-secondary:hover {
background-color: var(--p-surface-card);
border-color: var(--p-brand-primary);
color: var(--p-brand-primary);
transform: translateY(-1px);
}
.preview-btn-line {
background-color: var(--p-line-primary);
color: #FFFFFF;
border-radius: var(--p-radius-base);
transition: all 0.2s ease;
}
.preview-btn-line:hover {
background-color: var(--p-line-primary-hover);
transform: translateY(-1.5px);
box-shadow: 0 4px 12px rgba(0,185,0,0.25);
}
.preview-card-item {
background-color: var(--p-surface-card);
border: 1px solid var(--p-border-color);
border-radius: var(--p-radius-card);
box-shadow: var(--p-shadow-card);
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.preview-card-item:hover {
box-shadow: var(--p-shadow-card-hover);
transform: translateY(-3px);
}
.preview-nav-arrow {
background-color: color-mix(in srgb, var(--p-surface-card) 85%, transparent);
color: var(--p-text-main);
border: 1px solid var(--p-border-color);
backdrop-filter: blur(8px);
transition: all 0.2s ease;
}
.preview-nav-arrow:hover {
background-color: var(--p-brand-primary);
color: #FFFFFF;
border-color: var(--p-brand-primary);
transform: scale(1.1);
}
`}</style>
<div
style={{
...previewCssVars,
zoom: zoomScale / 100,
}}
className={`preview-theme-scope transition-all duration-300 overflow-hidden border border-slate-800 shrink-0 ${
viewMode === 'mobile'
? 'w-[375px] rounded-[36px] shadow-2xl ring-6 ring-slate-800/80 my-2'
: 'w-full max-w-xl rounded-2xl shadow-xl my-2'
}`}
>
{/* Mobile Device Notch/Bar */}
{viewMode === 'mobile' && (
<div className="h-6 bg-black/40 flex items-center justify-between px-6 text-[10px] text-slate-400 font-mono select-none">
<span>9:41</span>
<div className="w-20 h-3 bg-black/60 rounded-full" />
<span>5G 100%</span>
</div>
)}
{/* ── 1. Navbar Simulation (surfaceNavRest) ── */}
<header
style={{
backgroundColor: 'var(--p-surface-nav-rest)',
borderColor: 'var(--p-border-color)',
}}
className="px-4 py-2.5 border-b flex items-center justify-between transition-colors"
>
<div className="flex items-center gap-3">
<span
style={{ color: 'var(--p-text-main)' }}
className="font-bold text-xs tracking-widest uppercase flex items-center gap-1.5"
>
<span>HOMAROMA</span>
</span>
{viewMode === 'desktop' && (
<nav className="hidden sm:flex items-center gap-3 text-[11px] font-semibold ml-2">
<span
style={{ color: 'var(--p-brand-primary)' }}
className="cursor-pointer border-b-2 pb-0.5"
>
Home
</span>
<span
style={{ color: 'var(--p-muted-text)' }}
className="cursor-pointer hover:opacity-80 transition"
>
Shop
</span>
</nav>
)}
</div>
<div className="flex items-center gap-2">
<button
type="button"
style={{ color: 'var(--p-text-main)' }}
className="p-1 rounded-full hover:bg-black/5 transition"
title="Search"
>
<Search className="w-3.5 h-3.5" />
</button>
<div className="relative">
<button
type="button"
style={{ color: 'var(--p-text-main)' }}
className="p-1 rounded-full hover:bg-black/5 transition"
title="Cart"
>
<ShoppingCart className="w-3.5 h-3.5" />
</button>
<span
style={{ backgroundColor: 'var(--p-brand-accent)', color: '#FFFFFF' }}
className="absolute -top-1 -right-1 h-3.5 w-3.5 rounded-full text-[8px] font-bold flex items-center justify-center shadow-sm"
>
2
</span>
</div>
<button
type="button"
className="preview-btn-line px-2.5 py-1 text-[10px] font-bold flex items-center gap-1 cursor-pointer"
>
<MessageCircle className="w-3 h-3" />
<span>LINE</span>
</button>
</div>
</header>
{/* ── 2. Content Body Simulation (surfacePage) ── */}
<div className="p-4 sm:p-5 space-y-4">
{activeTab === 'storefront' ? (
<>
{/* ── Hero Slide / Carousel Banner ── */}
<div
style={{
backgroundColor: 'var(--p-surface-card)',
borderColor: 'var(--p-border-color)',
borderRadius: 'var(--p-radius-card)',
boxShadow: 'var(--p-shadow-card)'
}}
className={`relative border overflow-hidden transition-all duration-300 bg-gradient-to-br ${activeSlide.gradient}`}
>
{/* Interactive Prev/Next Navigation Buttons */}
<button
type="button"
onClick={() => setCurrentSlide(prev => (prev === 0 ? heroSlides.length - 1 : prev - 1))}
className="preview-nav-arrow absolute left-2 top-1/2 -translate-y-1/2 z-10 w-6 h-6 rounded-full flex items-center justify-center cursor-pointer shadow-md"
title="Previous Slide"
>
<ChevronLeft className="w-3.5 h-3.5" />
</button>
<button
type="button"
onClick={() => setCurrentSlide(prev => (prev === heroSlides.length - 1 ? 0 : prev + 1))}
className="preview-nav-arrow absolute right-2 top-1/2 -translate-y-1/2 z-10 w-6 h-6 rounded-full flex items-center justify-center cursor-pointer shadow-md"
title="Next Slide"
>
<ChevronRight className="w-3.5 h-3.5" />
</button>
{/* Slide Body Content */}
<div className="px-8 py-5 text-center space-y-2.5">
{/* Slide Eyebrow Badge */}
<div className="flex justify-center">
<span
style={{
backgroundColor: activeSlide.tagBg,
color: activeSlide.tagColor
}}
className="inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider shadow-sm transition-all"
>
<span>{activeSlide.icon}</span>
<span>{activeSlide.tag}</span>
</span>
</div>
{/* Slide Title */}
<h1
style={{ color: 'var(--p-text-main)' }}
className="text-lg md:text-xl font-bold tracking-tight transition-colors line-clamp-1"
>
{activeSlide.title}
</h1>
{/* Slide Subtitle */}
<p
style={{ color: 'var(--p-muted-text)' }}
className="text-xs leading-relaxed max-w-md mx-auto line-clamp-2"
>
{activeSlide.subtitle}
</p>
{/* Interactive Hero Buttons */}
<div className="flex flex-wrap items-center justify-center gap-2 pt-1">
{activeSlide.primaryBtnType === 'line' ? (
<button
type="button"
className="preview-btn-line px-3.5 py-1.5 text-[11px] font-bold tracking-wider uppercase flex items-center gap-1 cursor-pointer"
>
<MessageCircle className="w-3 h-3" />
<span>{activeSlide.primaryBtnText}</span>
</button>
) : (
<button
type="button"
className="preview-btn-primary px-3.5 py-1.5 text-[11px] font-bold tracking-wider uppercase cursor-pointer"
>
{activeSlide.primaryBtnText}
</button>
)}
<button
type="button"
className="preview-btn-secondary px-3.5 py-1.5 text-[11px] font-bold tracking-wider uppercase cursor-pointer"
>
{activeSlide.secondaryBtnText}
</button>
</div>
{/* Slide Indicator Dots */}
<div className="flex items-center justify-center gap-1.5 pt-2">
{heroSlides.map((s, idx) => (
<button
key={s.id}
type="button"
onClick={() => setCurrentSlide(idx)}
style={{
backgroundColor: idx === currentSlide ? 'var(--p-brand-primary)' : 'var(--p-border-color)'
}}
className={`h-1.5 rounded-full transition-all cursor-pointer ${
idx === currentSlide ? 'w-5 shadow-sm' : 'w-1.5 hover:opacity-80'
}`}
title={`Slide ${idx + 1}`}
/>
))}
</div>
</div>
</div>
{/* Product Cards Grid Showcase (surfaceCard) */}
<div className="grid gap-3.5 grid-cols-1 sm:grid-cols-2">
{/* Product Card 1 */}
<div className="preview-card-item p-3.5 space-y-2.5 cursor-pointer">
<div
style={{
backgroundColor: 'var(--p-surface-muted)',
borderRadius: 'calc(var(--p-radius-card) - 4px)',
borderColor: 'var(--p-border-color)'
}}
className="relative h-32 w-full flex items-center justify-center overflow-hidden border"
>
<span className="text-3xl select-none">🕯️</span>
{/* Sale Badge (brandAccent) */}
<span
style={{
backgroundColor: 'var(--p-brand-accent)',
color: '#FFFFFF'
}}
className="absolute top-2 left-2 px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider shadow-md"
>
SALE 25%
</span>
{/* In Stock Badge (brandSuccess) */}
<span
style={{
backgroundColor: 'var(--p-brand-success)',
color: '#111111'
}}
className="absolute bottom-2 right-2 px-1.5 py-0.5 rounded text-[8px] font-bold uppercase tracking-wider shadow-sm flex items-center gap-0.5"
>
<span>●</span> Ready
</span>
</div>
<div className="space-y-1">
{/* Rating Stars (brandWarm) */}
<div className="flex items-center gap-0.5">
{[1, 2, 3, 4, 5].map(s => (
<Star
key={s}
style={{
color: 'var(--p-brand-warm)',
fill: 'var(--p-brand-warm)'
}}
className="w-2.5 h-2.5"
/>
))}
<span
style={{ color: 'var(--p-caption-text)' }}
className="text-[9px] ml-1 font-mono"
>
5.0 (42)
</span>
</div>
{/* Title (textMain) */}
<h3
style={{ color: 'var(--p-text-main)' }}
className="text-xs font-bold truncate"
>
Midnight Lavender Candle
</h3>
{/* Description (mutedText) */}
<p
style={{ color: 'var(--p-muted-text)' }}
className="text-[11px] line-clamp-2 leading-relaxed"
>
กลิ่นลาเวนเดอร์บริสุทธิ์ ช่วยให้ผ่อนคลาย หลับสบายตลอดค่ำคืน
</p>
{/* Price & Action */}
<div className="flex items-center justify-between pt-1.5 border-t" style={{ borderColor: 'var(--p-border-color)' }}>
<div>
<div
style={{ color: 'var(--p-brand-primary)' }}
className="text-sm font-bold font-mono"
>
฿890
</div>
<div
style={{ color: 'var(--p-caption-text)' }}
className="text-[9px] line-through font-mono"
>
฿1,190
</div>
</div>
<button
type="button"
className="preview-btn-primary px-2.5 py-1 text-[10px] font-bold cursor-pointer"
>
Add to Cart
</button>
</div>
</div>
</div>
{/* Product Card 2 (Muted / Secondary Style) */}
<div className="preview-card-item p-3.5 space-y-2.5 cursor-pointer">
<div
style={{
backgroundColor: 'var(--p-surface-muted)',
borderRadius: 'calc(var(--p-radius-card) - 4px)',
borderColor: 'var(--p-border-color)'
}}
className="relative h-32 w-full flex items-center justify-center overflow-hidden border"
>
<span className="text-3xl select-none">🌿</span>
{/* Soft Highlight Badge (brandSoft) */}
<span
style={{
backgroundColor: 'var(--p-brand-soft)',
color: '#222222'
}}
className="absolute top-2 left-2 px-2 py-0.5 rounded-full text-[9px] font-bold uppercase tracking-wider shadow-sm"
>
ORGANIC
</span>
<span
style={{ color: 'var(--p-caption-text)' }}
className="absolute bottom-2 right-2 text-[8px] font-mono"
>
320 ชิ้น
</span>
</div>
<div className="space-y-1">
<div className="flex items-center gap-0.5">
{[1, 2, 3, 4, 5].map(s => (
<Star
key={s}
style={{
color: 'var(--p-brand-warm)',
fill: s <= 4 ? 'var(--p-brand-warm)' : 'none'
}}
className="w-2.5 h-2.5"
/>
))}
<span
style={{ color: 'var(--p-caption-text)' }}
className="text-[9px] ml-1 font-mono"
>
4.8 (89)
</span>
</div>
<h3
style={{ color: 'var(--p-text-main)' }}
className="text-xs font-bold truncate"
>
Eucalyptus Essential Oil
</h3>
<p
style={{ color: 'var(--p-muted-text)' }}
className="text-[11px] line-clamp-2 leading-relaxed"
>
น้ำมันหอมสกัดบริสุทธิ์ 100% หายใจโล่งสดชื่น
</p>
<div className="flex items-center justify-between pt-1.5 border-t" style={{ borderColor: 'var(--p-border-color)' }}>
<div
style={{ color: 'var(--p-brand-primary)' }}
className="text-sm font-bold font-mono"
>
฿650
</div>
<button
type="button"
className="preview-btn-line px-2.5 py-1 text-[10px] font-bold flex items-center gap-0.5 cursor-pointer"
>
<MessageCircle className="w-3 h-3" />
<span>LINE</span>
</button>
</div>
</div>
</div>
</div>
{/* Muted Section / Banner Showcase (surfaceMuted) */}
<div
style={{
backgroundColor: 'var(--p-surface-muted)',
borderColor: 'var(--p-border-color)',
borderRadius: 'var(--p-radius-card)'
}}
className="p-3.5 border flex items-center justify-between gap-3 text-left"
>
<div className="space-y-0.5">
<span
style={{ color: 'var(--p-caption-text)' }}
className="text-[9px] font-bold uppercase tracking-widest block"
>
Muted Section
</span>
<h4
style={{ color: 'var(--p-text-main)' }}
className="text-xs font-bold"
>
รับสิทธิ์ VIP Member ลดทันที 15%
</h4>
</div>
<button
type="button"
className="preview-btn-primary px-3 py-1.5 text-[10px] font-bold uppercase whitespace-nowrap cursor-pointer shrink-0"
>
Join
</button>
</div>
</>
) : (
/* ── Component Matrix View ── */
<div className="space-y-4">
<div className="p-3.5 rounded-xl border" style={{ backgroundColor: 'var(--p-surface-card)', borderColor: 'var(--p-border-color)' }}>
<h4 className="text-[11px] font-bold uppercase tracking-wider mb-2" style={{ color: 'var(--p-text-main)' }}>
Buttons & Hover Demonstration
</h4>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
<div className="space-y-1 text-center">
<button type="button" className="preview-btn-primary w-full py-2 text-[11px] font-bold uppercase cursor-pointer">
Primary CTA
</button>
<span className="text-[9px] block font-mono" style={{ color: 'var(--p-caption-text)' }}>
brandPrimary → Hover
</span>
</div>
<div className="space-y-1 text-center">
<button type="button" className="preview-btn-secondary w-full py-2 text-[11px] font-bold uppercase cursor-pointer">
Outline Action
</button>
<span className="text-[9px] block font-mono" style={{ color: 'var(--p-caption-text)' }}>
borderColor & textMain
</span>
</div>
<div className="space-y-1 text-center">
<button type="button" className="preview-btn-line w-full py-2 text-[11px] font-bold uppercase flex items-center justify-center gap-1 cursor-pointer">
<MessageCircle className="w-3 h-3" />
<span>LINE</span>
</button>
<span className="text-[9px] block font-mono" style={{ color: 'var(--p-caption-text)' }}>
linePrimary → Hover
</span>
</div>
</div>
</div>
{/* Badges & Accents Palette */}
<div className="p-3.5 rounded-xl border" style={{ backgroundColor: 'var(--p-surface-card)', borderColor: 'var(--p-border-color)' }}>
<h4 className="text-[11px] font-bold uppercase tracking-wider mb-2" style={{ color: 'var(--p-text-main)' }}>
Badges & Accents
</h4>
<div className="flex flex-wrap gap-1.5">
<span className="px-2 py-0.5 rounded-full text-[10px] font-bold shadow-sm" style={{ backgroundColor: 'var(--p-brand-accent)', color: '#FFFFFF' }}>
brandAccent (Sale)
</span>
<span className="px-2 py-0.5 rounded-full text-[10px] font-bold shadow-sm" style={{ backgroundColor: 'var(--p-brand-success)', color: '#111111' }}>
brandSuccess (Ready)
</span>
<span className="px-2 py-0.5 rounded-full text-[10px] font-bold shadow-sm" style={{ backgroundColor: 'var(--p-brand-warm)', color: '#111111' }}>
brandWarm (Rating ⭐)
</span>
<span className="px-2 py-0.5 rounded-full text-[10px] font-bold shadow-sm" style={{ backgroundColor: 'var(--p-brand-cool)', color: '#222222' }}>
brandCool (Tag)
</span>
<span className="px-2 py-0.5 rounded-full text-[10px] font-bold shadow-sm" style={{ backgroundColor: 'var(--p-brand-soft)', color: '#222222' }}>
brandSoft (Pastel)
</span>
</div>
</div>
{/* Typography Scale */}
<div className="p-3.5 rounded-xl border space-y-1.5" style={{ backgroundColor: 'var(--p-surface-card)', borderColor: 'var(--p-border-color)' }}>
<h4 className="text-[11px] font-bold uppercase tracking-wider mb-1" style={{ color: 'var(--p-text-main)' }}>
Typography Hierarchy
</h4>
<div className="text-sm font-bold" style={{ color: 'var(--p-text-main)' }}>
textMain — ข้อความหัวข้อหลัก H1-H3
</div>
<div className="text-xs" style={{ color: 'var(--p-muted-text)' }}>
mutedText — คำอธิบายสินค้า และเนื้อหารอง
</div>
<div className="text-[10px] font-mono" style={{ color: 'var(--p-caption-text)' }}>
captionText — หมายเหตุ วันที่ รหัสสินค้า และสถิติ
</div>
</div>
</div>
)}
</div>
</div>
</div>
</>
)}
</div>
)
}
+203
View File
@@ -0,0 +1,203 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { KeyRound, ShieldAlert, CheckCircle2, Loader2, ArrowRight, Database, UserCheck, Layers } from 'lucide-react'
import { API_BASE } from '@homaroma/types'
export default function InstallSeedPage() {
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const [result, setResult] = useState<any>(null)
const navigate = useNavigate()
const handleRunSeed = async (e: React.FormEvent) => {
e.preventDefault()
setError('')
setResult(null)
setLoading(true)
try {
const response = await fetch(`${API_BASE}/auth/install-seed`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ password }),
})
const data = await response.json()
if (!response.ok || !data.success) {
throw new Error(data.error || 'Failed to run installer seed')
}
setResult(data)
} catch (err: any) {
setError(err.message || 'Connection failed')
} finally {
setLoading(false)
}
}
return (
<div className="min-h-screen flex items-center justify-center p-6 bg-night-950 relative overflow-hidden">
{/* Background Glows */}
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-emerald-500/10 blur-[140px] rounded-full -translate-y-1/2 translate-x-1/2 pointer-events-none" />
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-cyan-glow/5 blur-[140px] rounded-full translate-y-1/2 -translate-x-1/2 pointer-events-none" />
<div className="w-full max-w-lg space-y-8 relative z-10 animate-fade-in">
{/* Header */}
<div className="text-center space-y-2">
<div className="inline-flex items-center gap-1.5 px-4 py-1.5 rounded-full bg-amber-500/10 border border-amber-500/20 text-[11px] font-bold uppercase tracking-[0.2em] text-amber-400 mb-2">
<ShieldAlert className="w-3.5 h-3.5" /> Emergency System Installer
</div>
<h1 className="text-3xl font-extrabold tracking-tight text-white">
HomAroma <span className="text-emerald-400">Seed & Recovery</span>
</h1>
<p className="text-slate-400 text-sm">
ระบบกู้คืนและติดตั้งข้อมูลตั้งต้น (Admin User, เมนูหลัก, หมวดหมู่, แบรนด์กลิ่น, และ CMS Pages)
</p>
</div>
{/* Card Form */}
<div className="admin-card border-white/10 shadow-2xl p-6 sm:p-8 space-y-6 bg-night-900/90 backdrop-blur-xl rounded-2xl">
{error && (
<div className="flex items-start gap-3 p-4 bg-rose-500/10 border border-rose-500/20 rounded-xl text-rose-400 text-sm animate-shake">
<ShieldAlert className="w-5 h-5 shrink-0 mt-0.5 text-rose-500" />
<div>
<p className="font-semibold">เกิดข้อผิดพลาดในการติดตั้ง</p>
<p className="text-xs text-rose-300/80 mt-0.5">{error}</p>
</div>
</div>
)}
{!result ? (
<form onSubmit={handleRunSeed} className="space-y-6">
<div className="space-y-2">
<label className="text-[11px] uppercase font-bold tracking-widest text-slate-400 ml-1">
กรอกรหัสผ่าน / Authorization PIN
</label>
<div className="relative group">
<KeyRound className="absolute left-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500 group-focus-within:text-emerald-400 transition-colors" />
<input
type="password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full pl-11 pr-4 py-3 bg-night-950/80 border border-white/10 focus:border-emerald-500/50 rounded-xl outline-none text-white transition-all font-mono tracking-wider"
placeholder="รหัสผ่านยืนยัน (เช่น 66195221)"
autoFocus
/>
</div>
<p className="text-[11px] text-slate-500 ml-1">
* ป้องกันการรันซ้ำโดยไม่ได้ตั้งใจ กรุณากรอกรหัสผ่านเพื่อดำเนินการ
</p>
</div>
<div className="bg-night-950/60 border border-white/5 rounded-xl p-4 space-y-2.5 text-xs text-slate-300">
<div className="font-semibold text-slate-200 flex items-center gap-1.5">
<Database className="w-4 h-4 text-emerald-400" /> รายการที่จะถูกสร้าง/กู้คืนอัตโนมัติ:
</div>
<ul className="space-y-1 text-slate-400 list-disc list-inside ml-1 text-[11px]">
<li>สร้างบัญชี <b className="text-slate-200">admin</b> (รหัสผ่านเริ่มต้น: <code className="text-emerald-400">homaroma123</code>)</li>
<li>สร้างโครงสร้างเมนูหลัก (Home, Shop, Scent Guide, About, FAQ, Track Order)</li>
<li>สร้าง 45 กลิ่น/แบรนด์น้ำมันหอมระเหย และ 5 หมวดหมู่สินค้า</li>
<li>สร้างหน้า CMS Dynamic Pages (Home, Shop Layout, Wishlist, Checkout ฯลฯ)</li>
</ul>
</div>
<button
type="submit"
disabled={loading || !password}
className="w-full py-3.5 px-4 bg-emerald-600 hover:bg-emerald-500 active:scale-[0.99] disabled:opacity-50 text-white font-semibold rounded-xl flex items-center justify-center gap-2 transition-all shadow-lg shadow-emerald-950/40 text-sm cursor-pointer"
>
{loading ? (
<>
<Loader2 className="w-4 h-4 animate-spin" />
กำลังดำเนินการ Seed ข้อมูลลง Database...
</>
) : (
<>
<Database className="w-4 h-4" />
🚀 รัน Emergency System Seed เดี๋ยวนี้
</>
)}
</button>
</form>
) : (
<div className="space-y-6 animate-fade-in">
<div className="flex items-center gap-3 p-4 bg-emerald-500/10 border border-emerald-500/20 rounded-xl text-emerald-400 text-sm">
<CheckCircle2 className="w-6 h-6 shrink-0 text-emerald-400" />
<div>
<h4 className="font-bold text-white text-base">ติดตั้งและกู้คืนข้อมูลสำเร็จ!</h4>
<p className="text-xs text-emerald-300/80 mt-0.5">{result.message}</p>
</div>
</div>
{/* Admin credentials card */}
<div className="bg-night-950/80 border border-emerald-500/20 rounded-xl p-4 space-y-3">
<div className="flex items-center gap-2 text-xs font-bold uppercase tracking-wider text-emerald-400">
<UserCheck className="w-4 h-4" /> ข้อมูลเข้าสู่ระบบ Admin:
</div>
<div className="grid grid-cols-2 gap-3 text-xs bg-black/40 p-3 rounded-lg border border-white/5">
<div>
<span className="text-slate-500 block text-[10px] uppercase font-bold">Username</span>
<span className="text-white font-mono font-bold text-sm">admin</span>
</div>
<div>
<span className="text-slate-500 block text-[10px] uppercase font-bold">Password</span>
<span className="text-emerald-400 font-mono font-bold text-sm">homaroma123</span>
</div>
</div>
</div>
{/* Stats summary */}
{result.summary && (
<div className="grid grid-cols-3 gap-2 text-center text-xs">
<div className="bg-night-950/40 border border-white/5 p-2.5 rounded-lg">
<span className="text-slate-500 block text-[10px]">Brands / กลิ่น</span>
<span className="font-bold text-white text-sm">{result.summary.brandsCount || 45}</span>
</div>
<div className="bg-night-950/40 border border-white/5 p-2.5 rounded-lg">
<span className="text-slate-500 block text-[10px]">Categories</span>
<span className="font-bold text-white text-sm">{result.summary.categoriesCount || 5}</span>
</div>
<div className="bg-night-950/40 border border-white/5 p-2.5 rounded-lg">
<span className="text-slate-500 block text-[10px]">CMS Pages</span>
<span className="font-bold text-white text-sm">{result.summary.pagesCount || 14}</span>
</div>
</div>
)}
<div className="flex flex-col sm:flex-row gap-3 pt-2">
<button
type="button"
onClick={() => navigate('/login')}
className="flex-1 py-3.5 px-4 bg-emerald-600 hover:bg-emerald-500 text-white font-semibold rounded-xl flex items-center justify-center gap-2 transition-all shadow-lg text-sm cursor-pointer"
>
ไปที่หน้าเข้าสู่ระบบ Admin <ArrowRight className="w-4 h-4" />
</button>
<button
type="button"
onClick={() => { setResult(null); setPassword(''); }}
className="py-3.5 px-4 bg-white/5 hover:bg-white/10 text-slate-300 font-medium rounded-xl transition-all text-sm cursor-pointer"
>
รันใหม่อีกครั้ง
</button>
</div>
</div>
)}
</div>
{/* Back to Login link */}
<div className="text-center">
<button
type="button"
onClick={() => navigate('/login')}
className="text-xs text-slate-500 hover:text-slate-300 transition-colors inline-flex items-center gap-1 cursor-pointer"
>
← กลับไปที่หน้าเข้าสู่ระบบปกติ (Login)
</button>
</div>
</div>
</div>
)
}
+12 -3
View File
@@ -129,9 +129,18 @@ export default function LoginPage() {
</button>
</form>
<p className="text-center text-[10px] text-slate-600 uppercase tracking-widest">
&copy; {new Date().getFullYear()} {brandName}
</p>
<div className="flex flex-col items-center gap-2">
<button
type="button"
onClick={() => navigate('/install-seed')}
className="text-[11px] text-slate-500 hover:text-emerald-400 transition-colors cursor-pointer flex items-center gap-1"
>
⚙️ ติดตั้งระบบ / กู้คืนข้อมูลเริ่มต้น (Emergency Seed)
</button>
<p className="text-center text-[10px] text-slate-600 uppercase tracking-widest">
&copy; {new Date().getFullYear()} {brandName}
</p>
</div>
</div>
</div>
)
+248 -61
View File
@@ -1,5 +1,6 @@
import { useState, useEffect, useRef } from 'react'
import { API_BASE, type StoreSettings } from '@homaroma/types'
import ThemeLivePreview from '../components/ThemeLivePreview'
import {
Save,
Loader2,
@@ -468,7 +469,7 @@ export default function ThemeSettingsPage() {
if (loading) return <div className="flex items-center justify-center p-20"><Loader2 className="w-8 h-8 text-cyan-glow animate-spin" /></div>
return (
<div className="max-w-4xl space-y-8 animate-fade-in pb-20">
<div className="w-full space-y-8 animate-fade-in pb-20">
{/* Top Header & Action Buttons */}
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 border-b border-white/5 pb-6">
@@ -705,97 +706,275 @@ export default function ThemeSettingsPage() {
{/* Main Settings Form */}
<form onSubmit={handleSave} className="space-y-8">
{/* ── 1. Color Palette Tokens ── */}
<div className="admin-card p-6 md:p-8 space-y-6">
<div className="grid grid-cols-1 xl:grid-cols-12 gap-8 items-start">
{/* ── Left Column: Color Palette & Surfaces & Settings ── */}
<div className="xl:col-span-7 space-y-8">
{/* ── 1. Color Palette Tokens ── */}
<div className="admin-card p-6 md:p-8 space-y-6">
<div className="flex items-center gap-3 border-b border-white/10 pb-4">
<div className="p-2 bg-cyan-glow/10 rounded-xl text-cyan-glow">
<span className="text-xl">🎨</span>
</div>
<div>
<h2 className="text-lg font-bold text-white">Color Palette & Surfaces</h2>
<p className="text-xs text-slate-400">Core brand colors, background surfaces, text hierarchy, and borders</p>
<p className="text-xs text-slate-400">Core brand colors, background surfaces, text hierarchy, and borders with micro-annotations</p>
</div>
</div>
{/* Group 1: Brand & Accent Colors */}
<div className="space-y-3">
<h3 className="text-xs font-bold uppercase tracking-wider text-cyan-glow">Brand & Accent Colors</h3>
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-4">
<div className="flex items-center justify-between">
<h3 className="text-xs font-bold uppercase tracking-wider text-cyan-glow">Brand & Accent Colors</h3>
<span className="text-[11px] text-slate-500">ปุ่มหลัก, ป้ายโปรโมชั่น และสีไฮไลต์ประจำแบรนด์</span>
</div>
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
{[
{ key: 'brandPrimary', label: 'Brand Primary' },
{ key: 'brandPrimaryHover', label: 'Primary Hover' },
{ key: 'brandAccent', label: 'Brand Accent' },
{ key: 'brandWarm', label: 'Brand Warm' },
{ key: 'brandCool', label: 'Brand Cool' },
{ key: 'brandSuccess', label: 'Brand Success' },
{ key: 'brandSoft', label: 'Brand Soft' },
].map(({ key, label }) => (
<div key={key} className="space-y-1.5 p-3 rounded-xl bg-white/[0.02] border border-white/5">
<div className="flex justify-between items-center text-[11px] font-semibold text-slate-300">
<span>{label}</span>
<span className="font-mono text-[10px] text-slate-500 uppercase">{themeTokens[key] || '#000'}</span>
{
key: 'brandPrimary',
label: 'Brand Primary',
role: 'ปุ่มหลัก (Primary CTA) & Active Nav',
desc: 'สีปุ่มดำเนินการหลัก เช่น เพิ่มลงตะกร้า, ลิงก์เมนูที่ถูกเลือก',
badgeColor: 'text-cyan-glow bg-cyan-glow/10 border-cyan-glow/20'
},
{
key: 'brandPrimaryHover',
label: 'Primary Hover',
role: 'สีปุ่มหลักเมื่อชี้เมาส์ (Hover State)',
desc: 'เฉดสีปุ่มเมื่อผู้ใช้ Hover เมาส์เหนือปุ่ม Primary CTA',
badgeColor: 'text-amber-400 bg-amber-400/10 border-amber-400/20'
},
{
key: 'brandAccent',
label: 'Brand Accent',
role: 'ป้ายโปรโมชั่น (Sale Badge) & ไฮไลต์',
desc: 'ป้ายลดราคา, ตัวเลขแจ้งเตือนในตะกร้า, จุดเด่นสะดุดตา',
badgeColor: 'text-rose-400 bg-rose-400/10 border-rose-400/20'
},
{
key: 'brandWarm',
label: 'Brand Warm',
role: 'ดาวรีวิว (Rating ⭐) & แสงอบอุ่น',
desc: 'ดาวแสดงคะแนนรีวิว, ไฮไลต์ประกายโทนอุ่น',
badgeColor: 'text-amber-300 bg-amber-300/10 border-amber-300/20'
},
{
key: 'brandCool',
label: 'Brand Cool',
role: 'ป้ายแท็กโทนเย็น & พื้นหลังสดชื่น',
desc: 'พื้นหลังแท็กสินค้าใหม่ (New Arrival), หมวดพิเศษ',
badgeColor: 'text-sky-400 bg-sky-400/10 border-sky-400/20'
},
{
key: 'brandSuccess',
label: 'Brand Success',
role: 'สถานะพร้อมส่ง (In Stock) & ชำระสำเร็จ',
desc: 'ป้ายสินค้าพร้อมส่ง, สัญลักษณ์การทำงานสำเร็จ',
badgeColor: 'text-emerald-400 bg-emerald-400/10 border-emerald-400/20'
},
{
key: 'brandSoft',
label: 'Brand Soft',
role: 'ไฮไลต์พาสเทล / ป้าย Organic',
desc: 'พื้นหลัง Badge สินค้าออร์แกนิก, กรอบไฮไลต์ละมุน',
badgeColor: 'text-purple-300 bg-purple-300/10 border-purple-300/20'
},
].map(({ key, label, role, desc, badgeColor }) => (
<div key={key} className="space-y-2 p-3.5 rounded-2xl bg-white/[0.02] border border-white/5 hover:border-white/15 transition-all flex flex-col justify-between">
<div className="space-y-1">
<div className="flex items-start justify-between gap-1.5">
<span className="text-xs font-bold text-white tracking-wide">{label}</span>
<div
className="w-4 h-4 rounded-full border border-white/20 shadow-sm shrink-0 mt-0.5"
style={{ backgroundColor: themeTokens[key] || '#000000' }}
/>
</div>
<div className={`inline-block px-1.5 py-0.5 rounded-md text-[10px] font-semibold border ${badgeColor}`}>
{role}
</div>
<p className="text-[11px] text-slate-400 leading-tight pt-0.5">
{desc}
</p>
</div>
<div className="flex items-center gap-2 pt-2 border-t border-white/5">
<input
type="color"
value={themeTokens[key] || '#000000'}
onChange={e => setThemeTokens(cur => ({ ...cur, [key]: e.target.value }))}
className="h-8 w-10 rounded-lg border border-white/10 bg-night-950 p-0.5 cursor-pointer shrink-0"
/>
<input
type="text"
value={themeTokens[key] || ''}
onChange={e => setThemeTokens(cur => ({ ...cur, [key]: e.target.value }))}
className="h-8 w-full rounded-lg border border-white/10 bg-night-950 px-2 font-mono text-[11px] text-slate-200 uppercase outline-none focus:border-cyan-glow transition"
placeholder="#000000"
/>
</div>
<input
type="color"
value={themeTokens[key] || '#000000'}
onChange={e => setThemeTokens(cur => ({ ...cur, [key]: e.target.value }))}
className="h-10 w-full rounded-lg border border-white/10 bg-night-950 p-1 cursor-pointer"
/>
</div>
))}
</div>
</div>
{/* Group 2: Surfaces & Backgrounds */}
<div className="space-y-3 pt-4 border-t border-white/5">
<h3 className="text-xs font-bold uppercase tracking-wider text-purple-400">Surfaces & Backgrounds</h3>
<div className="space-y-3 pt-6 border-t border-white/5">
<div className="flex items-center justify-between">
<h3 className="text-xs font-bold uppercase tracking-wider text-purple-400">Surfaces & Backgrounds</h3>
<span className="text-[11px] text-slate-500">มิติและเลเยอร์ของพื้นหลังหน้าเว็บ และกล่องเนื้อหา</span>
</div>
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-4">
{[
{ key: 'surfacePage', label: 'Page Background' },
{ key: 'surfaceNavRest', label: 'Navbar Background' },
{ key: 'surfaceCard', label: 'Card Surface' },
{ key: 'surfaceMuted', label: 'Muted Section Surface' },
].map(({ key, label }) => (
<div key={key} className="space-y-1.5 p-3 rounded-xl bg-white/[0.02] border border-white/5">
<div className="flex justify-between items-center text-[11px] font-semibold text-slate-300">
<span>{label}</span>
<span className="font-mono text-[10px] text-slate-500 uppercase">{themeTokens[key] || '#000'}</span>
{
key: 'surfacePage',
label: 'Page Background',
role: 'พื้นหลังหลักของเว็บไซต์ (Body)',
desc: 'สีผืนใหญ่ด้านหลังสุดที่คลุมทั้งหน้าเว็บ',
badgeColor: 'text-purple-300 bg-purple-300/10 border-purple-300/20'
},
{
key: 'surfaceNavRest',
label: 'Navbar Background',
role: 'พื้นหลังแถบเมนูด้านบน (Header)',
desc: 'สีแถบเมนูด้านบนสุดก่อนเลื่อนหน้าจอ',
badgeColor: 'text-indigo-300 bg-indigo-300/10 border-indigo-300/20'
},
{
key: 'surfaceCard',
label: 'Card Surface',
role: 'พื้นผิวกล่องการ์ดสินค้า & Dialog',
desc: 'สีพื้นหลังของกล่องสินค้าแต่ละชิ้น และกล่องข้อความ',
badgeColor: 'text-pink-300 bg-pink-300/10 border-pink-300/20'
},
{
key: 'surfaceMuted',
label: 'Muted Section Surface',
role: 'พื้นผิว Section สลับลาย & Footer',
desc: 'สีพื้นหลังของแถบข้อความโปรโมชั่น, แถบรีวิว, และส่วนท้าย',
badgeColor: 'text-fuchsia-300 bg-fuchsia-300/10 border-fuchsia-300/20'
},
].map(({ key, label, role, desc, badgeColor }) => (
<div key={key} className="space-y-2 p-3.5 rounded-2xl bg-white/[0.02] border border-white/5 hover:border-white/15 transition-all flex flex-col justify-between">
<div className="space-y-1">
<div className="flex items-start justify-between gap-1.5">
<span className="text-xs font-bold text-white tracking-wide">{label}</span>
<div
className="w-4 h-4 rounded-full border border-white/20 shadow-sm shrink-0 mt-0.5"
style={{ backgroundColor: themeTokens[key] || '#000000' }}
/>
</div>
<div className={`inline-block px-1.5 py-0.5 rounded-md text-[10px] font-semibold border ${badgeColor}`}>
{role}
</div>
<p className="text-[11px] text-slate-400 leading-tight pt-0.5">
{desc}
</p>
</div>
<div className="flex items-center gap-2 pt-2 border-t border-white/5">
<input
type="color"
value={themeTokens[key] || '#000000'}
onChange={e => setThemeTokens(cur => ({ ...cur, [key]: e.target.value }))}
className="h-8 w-10 rounded-lg border border-white/10 bg-night-950 p-0.5 cursor-pointer shrink-0"
/>
<input
type="text"
value={themeTokens[key] || ''}
onChange={e => setThemeTokens(cur => ({ ...cur, [key]: e.target.value }))}
className="h-8 w-full rounded-lg border border-white/10 bg-night-950 px-2 font-mono text-[11px] text-slate-200 uppercase outline-none focus:border-cyan-glow transition"
placeholder="#000000"
/>
</div>
<input
type="color"
value={themeTokens[key] || '#000000'}
onChange={e => setThemeTokens(cur => ({ ...cur, [key]: e.target.value }))}
className="h-10 w-full rounded-lg border border-white/10 bg-night-950 p-1 cursor-pointer"
/>
</div>
))}
</div>
</div>
{/* Group 3: Text & Lines */}
<div className="space-y-3 pt-4 border-t border-white/5">
<h3 className="text-xs font-bold uppercase tracking-wider text-emerald-400">Typography & Borders</h3>
<div className="space-y-3 pt-6 border-t border-white/5">
<div className="flex items-center justify-between">
<h3 className="text-xs font-bold uppercase tracking-wider text-emerald-400">Typography & Borders</h3>
<span className="text-[11px] text-slate-500">ลำดับการอ่านข้อความ, เส้นกรอบ และปุ่มติดต่อ LINE</span>
</div>
<div className="grid gap-4 sm:grid-cols-2 md:grid-cols-3">
{[
{ key: 'textMain', label: 'Main Text' },
{ key: 'mutedText', label: 'Muted Text' },
{ key: 'captionText', label: 'Caption Text' },
{ key: 'borderColor', label: 'Border Color' },
{ key: 'linePrimary', label: 'LINE Primary' },
{ key: 'linePrimaryHover', label: 'LINE Hover' },
].map(({ key, label }) => (
<div key={key} className="space-y-1.5 p-3 rounded-xl bg-white/[0.02] border border-white/5">
<div className="flex justify-between items-center text-[11px] font-semibold text-slate-300">
<span>{label}</span>
<span className="font-mono text-[10px] text-slate-500 uppercase">{themeTokens[key] || '#000'}</span>
{
key: 'textMain',
label: 'Main Text',
role: 'ข้อความหัวข้อและเนื้อหาหลัก',
desc: 'ชื่อสินค้า, หัวข้อ H1-H3, เนื้อหาหลักที่อ่านชัดเจน',
badgeColor: 'text-emerald-400 bg-emerald-400/10 border-emerald-400/20'
},
{
key: 'mutedText',
label: 'Muted Text',
role: 'ข้อความรอง & คำอธิบายย่อย',
desc: 'คำบรรยายสินค้า, เมนูย่อย, รายละเอียดที่ไม่เน้น',
badgeColor: 'text-slate-300 bg-slate-300/10 border-slate-300/20'
},
{
key: 'captionText',
label: 'Caption Text',
role: 'หมายเหตุ, วันที่, ข้อมูลสถิติ',
desc: 'ตัวหนังสือขนาดเล็ก, วันที่รีวิว, ยอดขายสินค้า',
badgeColor: 'text-zinc-400 bg-zinc-400/10 border-zinc-400/20'
},
{
key: 'borderColor',
label: 'Border Color',
role: 'เส้นขอบการ์ด & เส้นแบ่ง Divider',
desc: 'เส้นคั่นแบ่งแถว, เส้นกรอบรอบกล่องสินค้าและฟอร์ม',
badgeColor: 'text-amber-200 bg-amber-200/10 border-amber-200/20'
},
{
key: 'linePrimary',
label: 'LINE Primary',
role: 'ปุ่มแชท LINE Official',
desc: 'สีปุ่มติดต่อสั่งซื้อทาง LINE (สีเขียวของแบรนด์ LINE)',
badgeColor: 'text-emerald-300 bg-emerald-300/10 border-emerald-300/20'
},
{
key: 'linePrimaryHover',
label: 'LINE Hover',
role: 'สีปุ่ม LINE เมื่อชี้เมาส์ (Hover)',
desc: 'เฉดสีปุ่ม LINE เมื่อผู้ใช้ Hover เมาส์',
badgeColor: 'text-emerald-500 bg-emerald-500/10 border-emerald-500/20'
},
].map(({ key, label, role, desc, badgeColor }) => (
<div key={key} className="space-y-2 p-3.5 rounded-2xl bg-white/[0.02] border border-white/5 hover:border-white/15 transition-all flex flex-col justify-between">
<div className="space-y-1">
<div className="flex items-start justify-between gap-1.5">
<span className="text-xs font-bold text-white tracking-wide">{label}</span>
<div
className="w-4 h-4 rounded-full border border-white/20 shadow-sm shrink-0 mt-0.5"
style={{ backgroundColor: themeTokens[key] || '#000000' }}
/>
</div>
<div className={`inline-block px-1.5 py-0.5 rounded-md text-[10px] font-semibold border ${badgeColor}`}>
{role}
</div>
<p className="text-[11px] text-slate-400 leading-tight pt-0.5">
{desc}
</p>
</div>
<div className="flex items-center gap-2 pt-2 border-t border-white/5">
<input
type="color"
value={themeTokens[key] || '#000000'}
onChange={e => setThemeTokens(cur => ({ ...cur, [key]: e.target.value }))}
className="h-8 w-10 rounded-lg border border-white/10 bg-night-950 p-0.5 cursor-pointer shrink-0"
/>
<input
type="text"
value={themeTokens[key] || ''}
onChange={e => setThemeTokens(cur => ({ ...cur, [key]: e.target.value }))}
className="h-8 w-full rounded-lg border border-white/10 bg-night-950 px-2 font-mono text-[11px] text-slate-200 uppercase outline-none focus:border-cyan-glow transition"
placeholder="#000000"
/>
</div>
<input
type="color"
value={themeTokens[key] || '#000000'}
onChange={e => setThemeTokens(cur => ({ ...cur, [key]: e.target.value }))}
className="h-10 w-full rounded-lg border border-white/10 bg-night-950 p-1 cursor-pointer"
/>
</div>
))}
</div>
@@ -1218,6 +1397,14 @@ export default function ThemeSettingsPage() {
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
<span>Save Theme Settings</span>
</button>
</div>
</div>
{/* ── Right Column: Live Interactive UI Preview (Sticky) ── */}
<div className="xl:col-span-5 xl:sticky xl:top-20 space-y-6">
<ThemeLivePreview tokens={themeTokens} />
</div>
</div>
</form>
</div>