x
This commit is contained in:
1 parent
3e742ac468
commit
3ac0cb4f53
2 files changed
+940
-114
No files matched your search
@@ -0,0 +1,221 @@
|
||||
# Prompt สำหรับ LLM: สร้าง Theme & Design Settings JSON สำหรับระบบ E-Commerce
|
||||
|
||||
ไฟล์นี้ใช้สำหรับ **Copy & Paste ส่งให้ LLM (ChatGPT / Claude / Gemini)** เพื่อสร้างชุด Color Tokens, Typography, Animation, และ Layout Settings ให้กับเว็บไซต์ `apps/website` และ `apps/spa` เพื่อนำไป Import หรือวาง (Paste JSON) ในหน้า **Admin -> Theme & Design**
|
||||
|
||||
---
|
||||
|
||||
## 📋 Prompt ส่งให้ LLM (Copy ข้อความด้านล่างนี้ทั้งหมด)
|
||||
|
||||
```text
|
||||
คุณคือ Senior UI/UX Designer และ Design System Architect ระดับมืออาชีพ เชี่ยวชาญการออกแบบ Color Palette, Dark/Light Theme System, และ Visual Identity ให้กับร้านค้าออนไลน์ระดับ Luxury, Minimalist, Cyberpunk, หรือ Modern Lifestyle
|
||||
|
||||
[ภารกิจของคุณ]:
|
||||
สร้าง Theme & Design Settings JSON ที่สมบูรณ์แบบ มีความกลมกลืนของสี (Color Harmony), Contrast Ratio ผ่านเกณฑ์ WCAG AA/AAA เพื่อนำเข้าสู่ระบบ E-commerce ในรูปแบบ Valid JSON Object ตาม Schema ที่กำหนดอย่างเคร่งครัด
|
||||
|
||||
[กฎและข้อกำหนดทางเทคนิค]:
|
||||
1. ผลลัพธ์ต้องเป็น Valid JSON Object ล้วนๆ เริ่มต้นด้วย { และจบด้วย } เท่านั้น (ห้ามมี Markdown Backticks หรือคำเกริ่นนำ)
|
||||
2. ค่าสีทั้งหมดต้องอยู่ในรูปแบบ HEX Color Code 6 หลัก (เช่น "#A100FF", "#0A0D14")
|
||||
3. โครงสร้าง JSON ต้องประกอบด้วย 5 ส่วนหลัก:
|
||||
- "version": "1.0"
|
||||
- "themeTokens": Object เก็บค่าสีและสไตล์ทั้งหมด
|
||||
- "scrollRevealEnabled": boolean (true/false)
|
||||
- "scrollRevealStyle": "fade-up" | "fade-in" | "slide-left" | "zoom-in" | "none"
|
||||
- "scrollRevealDuration": number (เช่น 600, 700, 800)
|
||||
- "headerStyle": "single_line" | "two_tier"
|
||||
|
||||
[รายละเอียดของ themeTokens แต่ละฟิลด์]:
|
||||
• กลุ่มสีแบรนด์และไฮไลต์ (Brand & Accents):
|
||||
- "brandPrimary": สีหลักของแบรนด์ (Primary CTA Button, Active Links)
|
||||
- "brandPrimaryHover": สีเมื่อชี้เมาส์ที่ปุ่มหลัก
|
||||
- "brandAccent": สีไฮไลต์สะดุดตา (Badge, Tag, Sale Discount)
|
||||
- "brandWarm": สีโทนอุ่น (Rating Star, Warm Glow)
|
||||
- "brandCool": สีโทนเย็น / Light contrast
|
||||
- "brandSuccess": สีแจ้งเตือนความสำเร็จ (In Stock, Completed)
|
||||
- "brandSoft": สีรองโทนอ่อน / Pastel highlight
|
||||
|
||||
• กลุ่มพื้นผิวและเลเยอร์ (Surfaces & Backgrounds):
|
||||
- "surfacePage": สีพื้นหลังหลักของหน้าเว็บ (เช่น "#0A0D14" สำหรับ Dark Mode หรือ "#F8FAFC" สำหรับ Light Mode)
|
||||
- "surfaceNavRest": สีพื้นหลังของ Header / Navbar
|
||||
- "surfaceCard": สีพื้นหลังของการ์ดสินค้า / Container Block
|
||||
- "surfaceMuted": สีพื้นหลังของ Section สลับลาย / Footer
|
||||
|
||||
• กลุ่มข้อความและการอ่าน (Typography Colors):
|
||||
- "textMain": สีข้อความหัวข้อและเนื้อหาหลัก (ต้อง Contrast ชัดเจนกับ surfacePage)
|
||||
- "mutedText": สีข้อความรอง / รายละเอียดปลีกย่อย
|
||||
- "captionText": สีข้อความกำกับ / ตัวเลขสถิติ / วันที่
|
||||
|
||||
• กลุ่มเส้นขอบและช่องทางติดต่อ (Borders & Social):
|
||||
- "borderColor": สีเส้นขอบการ์ดและเส้นแบ่ง Divider
|
||||
- "linePrimary": สีเขียว LINE Official หรือโทนสีไอคอนแชท
|
||||
- "linePrimaryHover": สีเมื่อ Hover ไอคอน LINE
|
||||
|
||||
• กลุ่มมิติและสเปซ (Elevations & Geometry):
|
||||
- "fontBody": ฟอนต์เนื้อหา (ปล่อยว่าง "" เพื่อใช้ค่าเริ่มต้นของระบบ หรือใส่ฟอนต์ Google Fonts)
|
||||
- "fontDisplay": ฟอนต์หัวข้อ (ปล่อยว่าง "" หรือใส่ฟอนต์ Serif/Display เช่น "Playfair Display")
|
||||
- "shadowCard": เงาของการ์ดปกติ (เช่น "0 2px 8px rgba(0,0,0,0.30)")
|
||||
- "shadowCardHover": เงาเมื่อ Hover การ์ด (เช่น "0 8px 24px rgba(0,0,0,0.40)")
|
||||
- "radiusBase": ความโค้งมนของปุ่ม/อินพุต (เช่น "8px", "12px", "9999px")
|
||||
- "radiusCard": ความโค้งมนของการ์ด (เช่น "16px", "20px", "24px")
|
||||
|
||||
---
|
||||
|
||||
[ตัวอย่าง JSON Template ที่ถูกต้อง]:
|
||||
{
|
||||
"version": "1.0",
|
||||
"themeTokens": {
|
||||
"brandPrimary": "#A100FF",
|
||||
"brandPrimaryHover": "#C23BFF",
|
||||
"brandAccent": "#FF7A1A",
|
||||
"brandWarm": "#FF8F45",
|
||||
"brandCool": "#E7EBF5",
|
||||
"brandSuccess": "#46D36A",
|
||||
"brandSoft": "#D16BFF",
|
||||
"surfacePage": "#0A0D14",
|
||||
"surfaceNavRest": "#0D1018",
|
||||
"surfaceCard": "#151A24",
|
||||
"textMain": "#F5F7FF",
|
||||
"mutedText": "#A7AFBF",
|
||||
"linePrimary": "#00B900",
|
||||
"linePrimaryHover": "#009900",
|
||||
"captionText": "#7D8598",
|
||||
"borderColor": "#242A38",
|
||||
"surfaceMuted": "#111622",
|
||||
"fontBody": "",
|
||||
"fontDisplay": "",
|
||||
"shadowCard": "0 2px 8px rgba(0,0,0,0.30)",
|
||||
"shadowCardHover": "0 8px 24px rgba(0,0,0,0.40)",
|
||||
"radiusBase": "8px",
|
||||
"radiusCard": "16px"
|
||||
},
|
||||
"scrollRevealEnabled": true,
|
||||
"scrollRevealStyle": "fade-up",
|
||||
"scrollRevealDuration": 700,
|
||||
"headerStyle": "single_line"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 ตัวอย่าง Preset สำเร็จรูปสำหรับใช้งานได้ทันที
|
||||
|
||||
### 1. Cyberpunk Dark Neon (สไตล์ Gaming, Gadget, Streetwear)
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"themeTokens": {
|
||||
"brandPrimary": "#A100FF",
|
||||
"brandPrimaryHover": "#C23BFF",
|
||||
"brandAccent": "#FF7A1A",
|
||||
"brandWarm": "#FF8F45",
|
||||
"brandCool": "#E7EBF5",
|
||||
"brandSuccess": "#46D36A",
|
||||
"brandSoft": "#D16BFF",
|
||||
"surfacePage": "#0A0D14",
|
||||
"surfaceNavRest": "#0D1018",
|
||||
"surfaceCard": "#151A24",
|
||||
"textMain": "#F5F7FF",
|
||||
"mutedText": "#A7AFBF",
|
||||
"linePrimary": "#00B900",
|
||||
"linePrimaryHover": "#009900",
|
||||
"captionText": "#7D8598",
|
||||
"borderColor": "#242A38",
|
||||
"surfaceMuted": "#111622",
|
||||
"fontBody": "",
|
||||
"fontDisplay": "",
|
||||
"shadowCard": "0 2px 8px rgba(0,0,0,0.30)",
|
||||
"shadowCardHover": "0 8px 24px rgba(0,0,0,0.40)",
|
||||
"radiusBase": "8px",
|
||||
"radiusCard": "16px"
|
||||
},
|
||||
"scrollRevealEnabled": true,
|
||||
"scrollRevealStyle": "fade-up",
|
||||
"scrollRevealDuration": 700,
|
||||
"headerStyle": "single_line"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Luxury Emerald & Gold (สไตล์ High-End Jewelry, Luxury Perfume)
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"themeTokens": {
|
||||
"brandPrimary": "#D4AF37",
|
||||
"brandPrimaryHover": "#E6C655",
|
||||
"brandAccent": "#0F52BA",
|
||||
"brandWarm": "#E5B94E",
|
||||
"brandCool": "#E8ECEF",
|
||||
"brandSuccess": "#2E8B57",
|
||||
"brandSoft": "#F5E6BE",
|
||||
"surfacePage": "#0B1512",
|
||||
"surfaceNavRest": "#0E1A16",
|
||||
"surfaceCard": "#14241F",
|
||||
"textMain": "#F8F9FA",
|
||||
"mutedText": "#A2B3AC",
|
||||
"linePrimary": "#00B900",
|
||||
"linePrimaryHover": "#009900",
|
||||
"captionText": "#6F857D",
|
||||
"borderColor": "#243A32",
|
||||
"surfaceMuted": "#0F1E19",
|
||||
"fontBody": "",
|
||||
"fontDisplay": "\"Playfair Display\", ui-serif, Georgia, serif",
|
||||
"shadowCard": "0 4px 12px rgba(0,0,0,0.35)",
|
||||
"shadowCardHover": "0 12px 32px rgba(212,175,55,0.15)",
|
||||
"radiusBase": "6px",
|
||||
"radiusCard": "14px"
|
||||
},
|
||||
"scrollRevealEnabled": true,
|
||||
"scrollRevealStyle": "fade-up",
|
||||
"scrollRevealDuration": 800,
|
||||
"headerStyle": "two_tier"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Clean Pastel Aesthetic (สไตล์ Organic Skincare, Bakery, Baby Care)
|
||||
```json
|
||||
{
|
||||
"version": "1.0",
|
||||
"themeTokens": {
|
||||
"brandPrimary": "#E07A5F",
|
||||
"brandPrimaryHover": "#D06346",
|
||||
"brandAccent": "#F4A261",
|
||||
"brandWarm": "#F6BD60",
|
||||
"brandCool": "#E9ECEF",
|
||||
"brandSuccess": "#588157",
|
||||
"brandSoft": "#F2CC8F",
|
||||
"surfacePage": "#FAF8F5",
|
||||
"surfaceNavRest": "#FFFFFF",
|
||||
"surfaceCard": "#FFFFFF",
|
||||
"textMain": "#2B2D42",
|
||||
"mutedText": "#6C757D",
|
||||
"linePrimary": "#00B900",
|
||||
"linePrimaryHover": "#009900",
|
||||
"captionText": "#8D99AE",
|
||||
"borderColor": "#EAE5DF",
|
||||
"surfaceMuted": "#F3EFEA",
|
||||
"fontBody": "",
|
||||
"fontDisplay": "",
|
||||
"shadowCard": "0 4px 16px rgba(0,0,0,0.04)",
|
||||
"shadowCardHover": "0 10px 28px rgba(0,0,0,0.08)",
|
||||
"radiusBase": "12px",
|
||||
"radiusCard": "24px"
|
||||
},
|
||||
"scrollRevealEnabled": true,
|
||||
"scrollRevealStyle": "fade-in",
|
||||
"scrollRevealDuration": 600,
|
||||
"headerStyle": "single_line"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 วิธีนำไปใช้งานในระบบ
|
||||
|
||||
1. **ขอให้ LLM สร้าง:** Copy ข้อความ Prompt ด้านบน แล้วส่งให้ LLM โดยสามารถระบุ Concept เพิ่มเติมได้ เช่น:
|
||||
- *"สร้างธีมโทน Rose Gold สำหรับร้านน้ำหอมผู้หญิง"*
|
||||
- *"สร้างธีม Minimalist Dark Mode สำหรับแบรนด์แฟชั่นญี่ปุ่น"*
|
||||
2. **คัดลอก JSON:** Copy ผลลัพธ์ JSON ที่ LLM สร้างขึ้น
|
||||
3. **นำเข้าใน Admin:**
|
||||
- ไปที่หน้า **Admin Panel -> Theme & Design**
|
||||
- คลิกปุ่ม **"Paste JSON"** (หรือ **"Import JSON"**)
|
||||
- วางโค้ด JSON ลงในกล่องข้อความ แล้วกด **"Apply to Form"**
|
||||
- กดปุ่ม **"Save Theme Settings"** เพื่อบันทึกใช้งานจริงทันที
|
||||
@@ -1,18 +1,44 @@
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { API_BASE, type StoreSettings } from '@homaroma/types'
|
||||
import { Save, Loader2, Sparkles, Download, Upload } from 'lucide-react'
|
||||
import {
|
||||
Save,
|
||||
Loader2,
|
||||
Sparkles,
|
||||
Download,
|
||||
Upload,
|
||||
Copy,
|
||||
ClipboardPaste,
|
||||
FileJson,
|
||||
Check,
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
Code2,
|
||||
X,
|
||||
Bot
|
||||
} from 'lucide-react'
|
||||
|
||||
const defaultThemeTokens = {
|
||||
brandPrimary: '#FF6B6B', brandPrimaryHover: '#FF4500', brandAccent: '#FFF200',
|
||||
brandWarm: '#FFA07A', brandCool: '#E0FFFF', brandSuccess: '#98FB98', brandSoft: '#D8BFD8',
|
||||
surfacePage: '#FFF0F5', surfaceNavRest: '#F9F8F5', surfaceCard: '#FFFFFF',
|
||||
textMain: '#333333', mutedText: '#666666',
|
||||
linePrimary: '#00B900', linePrimaryHover: '#009900',
|
||||
brandPrimary: '#FF6B6B',
|
||||
brandPrimaryHover: '#FF4500',
|
||||
brandAccent: '#FFF200',
|
||||
brandWarm: '#FFA07A',
|
||||
brandCool: '#E0FFFF',
|
||||
brandSuccess: '#98FB98',
|
||||
brandSoft: '#D8BFD8',
|
||||
surfacePage: '#FFF0F5',
|
||||
surfaceNavRest: '#F9F8F5',
|
||||
surfaceCard: '#FFFFFF',
|
||||
textMain: '#333333',
|
||||
mutedText: '#666666',
|
||||
linePrimary: '#00B900',
|
||||
linePrimaryHover: '#009900',
|
||||
captionText: '#B0A396',
|
||||
borderColor: '#E8E0D8',
|
||||
surfaceMuted: '#EDE6DD',
|
||||
fontBody: '', fontDisplay: '',
|
||||
shadowCard: '0 10px 25px rgba(0,0,0,0.05)', shadowCardHover: '0 20px 40px rgba(0,0,0,0.1)',
|
||||
fontBody: '',
|
||||
fontDisplay: '',
|
||||
shadowCard: '0 10px 25px rgba(0,0,0,0.05)',
|
||||
shadowCardHover: '0 20px 40px rgba(0,0,0,0.1)',
|
||||
radiusBase: '8px',
|
||||
radiusCard: '16px',
|
||||
}
|
||||
@@ -25,6 +51,90 @@ const FONT_PRESETS = {
|
||||
} as const
|
||||
type FontPresetKey = keyof typeof FONT_PRESETS
|
||||
|
||||
const LLM_PROMPT_TEXT = `คุณคือ Senior UI/UX Designer และ Design System Architect ระดับมืออาชีพ เชี่ยวชาญการออกแบบ Color Palette, Dark/Light Theme System, และ Visual Identity ให้กับร้านค้าออนไลน์ระดับ Luxury, Minimalist, Cyberpunk, หรือ Modern Lifestyle
|
||||
|
||||
[ภารกิจของคุณ]:
|
||||
สร้าง Theme & Design Settings JSON ที่สมบูรณ์แบบ มีความกลมกลืนของสี (Color Harmony), Contrast Ratio ผ่านเกณฑ์ WCAG AA/AAA เพื่อนำเข้าสู่ระบบ E-commerce ในรูปแบบ Valid JSON Object ตาม Schema ที่กำหนดอย่างเคร่งครัด
|
||||
|
||||
[กฎและข้อกำหนดทางเทคนิค]:
|
||||
1. ผลลัพธ์ต้องเป็น Valid JSON Object ล้วนๆ เริ่มต้นด้วย { และจบด้วย } เท่านั้น (ห้ามมี Markdown Backticks หรือคำเกริ่นนำ)
|
||||
2. ค่าสีทั้งหมดต้องอยู่ในรูปแบบ HEX Color Code 6 หลัก (เช่น "#A100FF", "#0A0D14")
|
||||
3. โครงสร้าง JSON ต้องประกอบด้วย:
|
||||
- "version": "1.0"
|
||||
- "themeTokens": Object เก็บค่าสีและสไตล์ทั้งหมด
|
||||
- "scrollRevealEnabled": boolean (true/false)
|
||||
- "scrollRevealStyle": "fade-up" | "fade-in" | "slide-left" | "zoom-in" | "none"
|
||||
- "scrollRevealDuration": number (เช่น 600, 700, 800)
|
||||
- "headerStyle": "single_line" | "two_tier"
|
||||
|
||||
[รายละเอียดของ themeTokens แต่ละฟิลด์]:
|
||||
• กลุ่มสีแบรนด์และไฮไลต์ (Brand & Accents):
|
||||
- "brandPrimary": สีหลักของแบรนด์ (Primary CTA Button, Active Links)
|
||||
- "brandPrimaryHover": สีเมื่อชี้เมาส์ที่ปุ่มหลัก
|
||||
- "brandAccent": สีไฮไลต์สะดุดตา (Badge, Tag, Sale Discount)
|
||||
- "brandWarm": สีโทนอุ่น (Rating Star, Warm Glow)
|
||||
- "brandCool": สีโทนเย็น / Light contrast
|
||||
- "brandSuccess": สีแจ้งเตือนความสำเร็จ (In Stock, Completed)
|
||||
- "brandSoft": สีรองโทนอ่อน / Pastel highlight
|
||||
|
||||
• กลุ่มพื้นผิวและเลเยอร์ (Surfaces & Backgrounds):
|
||||
- "surfacePage": สีพื้นหลังหลักของหน้าเว็บ
|
||||
- "surfaceNavRest": สีพื้นหลังของ Header / Navbar
|
||||
- "surfaceCard": สีพื้นหลังของการ์ดสินค้า / Container Block
|
||||
- "surfaceMuted": สีพื้นหลังของ Section สลับลาย / Footer
|
||||
|
||||
• กลุ่มข้อความและการอ่าน (Typography Colors):
|
||||
- "textMain": สีข้อความหัวข้อและเนื้อหาหลัก
|
||||
- "mutedText": สีข้อความรอง / รายละเอียดปลีกย่อย
|
||||
- "captionText": สีข้อความกำกับ / ตัวเลขสถิติ / วันที่
|
||||
|
||||
• กลุ่มเส้นขอบและช่องทางติดต่อ (Borders & Social):
|
||||
- "borderColor": สีเส้นขอบการ์ดและเส้นแบ่ง Divider
|
||||
- "linePrimary": สีเขียว LINE Official หรือโทนสีไอคอนแชท
|
||||
- "linePrimaryHover": สีเมื่อ Hover ไอคอน LINE
|
||||
|
||||
• กลุ่มมิติและสเปซ (Elevations & Geometry):
|
||||
- "fontBody": ฟอนต์เนื้อหา (ปล่อยว่าง "" หรือใส่ Google Fonts)
|
||||
- "fontDisplay": ฟอนต์หัวข้อ (ปล่อยว่าง "" หรือใส่ฟอนต์ Serif/Display)
|
||||
- "shadowCard": เงาของการ์ดปกติ
|
||||
- "shadowCardHover": เงาเมื่อ Hover การ์ด
|
||||
- "radiusBase": ความโค้งมนของปุ่ม/อินพุต (เช่น "8px")
|
||||
- "radiusCard": ความโค้งมนของการ์ด (เช่น "16px")
|
||||
|
||||
[ตัวอย่าง JSON Template]:
|
||||
{
|
||||
"version": "1.0",
|
||||
"themeTokens": {
|
||||
"brandPrimary": "#A100FF",
|
||||
"brandPrimaryHover": "#C23BFF",
|
||||
"brandAccent": "#FF7A1A",
|
||||
"brandWarm": "#FF8F45",
|
||||
"brandCool": "#E7EBF5",
|
||||
"brandSuccess": "#46D36A",
|
||||
"brandSoft": "#D16BFF",
|
||||
"surfacePage": "#0A0D14",
|
||||
"surfaceNavRest": "#0D1018",
|
||||
"surfaceCard": "#151A24",
|
||||
"textMain": "#F5F7FF",
|
||||
"mutedText": "#A7AFBF",
|
||||
"linePrimary": "#00B900",
|
||||
"linePrimaryHover": "#009900",
|
||||
"captionText": "#7D8598",
|
||||
"borderColor": "#242A38",
|
||||
"surfaceMuted": "#111622",
|
||||
"fontBody": "",
|
||||
"fontDisplay": "",
|
||||
"shadowCard": "0 2px 8px rgba(0,0,0,0.30)",
|
||||
"shadowCardHover": "0 8px 24px rgba(0,0,0,0.40)",
|
||||
"radiusBase": "8px",
|
||||
"radiusCard": "16px"
|
||||
},
|
||||
"scrollRevealEnabled": true,
|
||||
"scrollRevealStyle": "fade-up",
|
||||
"scrollRevealDuration": 700,
|
||||
"headerStyle": "single_line"
|
||||
}`
|
||||
|
||||
function resolveFontPreset(tokens: Record<string, string>) {
|
||||
const body = tokens.fontBody?.trim() || ''
|
||||
const display = tokens.fontDisplay?.trim() || ''
|
||||
@@ -46,6 +156,15 @@ export default function ThemeSettingsPage() {
|
||||
const [scrollRevealDuration, setScrollRevealDuration] = useState('700')
|
||||
const [headerStyle, setHeaderStyle] = useState('single_line')
|
||||
|
||||
// JSON Quick Copy / Paste State
|
||||
const [isJsonBoxOpen, setIsJsonBoxOpen] = useState(false)
|
||||
const [jsonInput, setJsonInput] = useState('')
|
||||
const [jsonError, setJsonError] = useState<string | null>(null)
|
||||
const [toastMessage, setToastMessage] = useState<string | null>(null)
|
||||
|
||||
// Show Prompt Modal State
|
||||
const [isPromptModalOpen, setIsPromptModalOpen] = useState(false)
|
||||
|
||||
const fontPreset = resolveFontPreset(themeTokens)
|
||||
|
||||
const applyFontPreset = (preset: FontPresetKey | 'custom') => {
|
||||
@@ -67,8 +186,14 @@ export default function ThemeSettingsPage() {
|
||||
.finally(() => setLoading(false))
|
||||
}, [])
|
||||
|
||||
const handleExport = () => {
|
||||
const data = {
|
||||
const showToast = (msg: string) => {
|
||||
setToastMessage(msg)
|
||||
setTimeout(() => setToastMessage(null), 2500)
|
||||
}
|
||||
|
||||
// Generate current JSON payload
|
||||
const getCurrentThemeJson = () => {
|
||||
return {
|
||||
version: '1.0',
|
||||
exportedAt: new Date().toISOString(),
|
||||
themeTokens,
|
||||
@@ -77,7 +202,152 @@ export default function ThemeSettingsPage() {
|
||||
scrollRevealDuration: parseInt(scrollRevealDuration) || 700,
|
||||
headerStyle,
|
||||
}
|
||||
const dataStr = JSON.stringify(data, null, 2)
|
||||
}
|
||||
|
||||
// ── Copy JSON to Clipboard ────────────────────────────────────────────────
|
||||
const handleCopyJson = async () => {
|
||||
try {
|
||||
const dataStr = JSON.stringify(getCurrentThemeJson(), null, 2)
|
||||
await navigator.clipboard.writeText(dataStr)
|
||||
showToast('Theme JSON copied to clipboard!')
|
||||
} catch (err) {
|
||||
console.error('Failed to copy to clipboard:', err)
|
||||
alert('Could not copy JSON to clipboard automatically. Please use Export JSON.')
|
||||
}
|
||||
}
|
||||
|
||||
// ── Copy LLM Prompt to Clipboard ──────────────────────────────────────────
|
||||
const handleCopyPrompt = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(LLM_PROMPT_TEXT)
|
||||
showToast('LLM Prompt copied to clipboard!')
|
||||
} catch (err) {
|
||||
console.error('Failed to copy prompt:', err)
|
||||
alert('Could not copy prompt to clipboard. Please copy manually.')
|
||||
}
|
||||
}
|
||||
|
||||
// ── Apply JSON from Text / Object ─────────────────────────────────────────
|
||||
const applyJsonData = (parsed: any) => {
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
throw new Error('Invalid JSON format: expected an object')
|
||||
}
|
||||
|
||||
// Extract tokens (support "tokens", "themeTokens", or root dictionary)
|
||||
const extractedTokens =
|
||||
(parsed.tokens && typeof parsed.tokens === 'object' && !Array.isArray(parsed.tokens)) ? parsed.tokens :
|
||||
(parsed.themeTokens && typeof parsed.themeTokens === 'object' && !Array.isArray(parsed.themeTokens)) ? parsed.themeTokens :
|
||||
(parsed.brandPrimary || parsed.surfacePage || parsed.textMain) ? parsed : null
|
||||
|
||||
if (extractedTokens) {
|
||||
setThemeTokens(cur => ({ ...cur, ...extractedTokens }))
|
||||
}
|
||||
|
||||
// Extract Scroll Reveal
|
||||
if (parsed.scrollReveal && typeof parsed.scrollReveal === 'object') {
|
||||
if (typeof parsed.scrollReveal.enabled === 'boolean') {
|
||||
setScrollRevealEnabled(parsed.scrollReveal.enabled)
|
||||
}
|
||||
if (typeof parsed.scrollReveal.style === 'string') {
|
||||
setScrollRevealStyle(parsed.scrollReveal.style)
|
||||
}
|
||||
if (parsed.scrollReveal.duration !== undefined) {
|
||||
setScrollRevealDuration(parsed.scrollReveal.duration.toString())
|
||||
}
|
||||
} else {
|
||||
if (typeof parsed.scrollRevealEnabled === 'boolean') {
|
||||
setScrollRevealEnabled(parsed.scrollRevealEnabled)
|
||||
}
|
||||
if (typeof parsed.scrollRevealStyle === 'string') {
|
||||
setScrollRevealStyle(parsed.scrollRevealStyle)
|
||||
}
|
||||
if (parsed.scrollRevealDuration !== undefined) {
|
||||
setScrollRevealDuration(parsed.scrollRevealDuration.toString())
|
||||
}
|
||||
}
|
||||
|
||||
// Extract Header Style
|
||||
if (parsed.header && typeof parsed.header === 'object' && typeof parsed.header.style === 'string') {
|
||||
setHeaderStyle(parsed.header.style)
|
||||
} else if (typeof parsed.headerStyle === 'string') {
|
||||
setHeaderStyle(parsed.headerStyle)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Paste JSON Text Handlers ──────────────────────────────────────────────
|
||||
const handleApplyJsonText = () => {
|
||||
setJsonError(null)
|
||||
if (!jsonInput.trim()) {
|
||||
setJsonError('Please enter valid JSON text')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(jsonInput)
|
||||
applyJsonData(parsed)
|
||||
showToast('Theme settings applied from JSON!')
|
||||
setIsJsonBoxOpen(false)
|
||||
setJsonInput('')
|
||||
} catch (err: any) {
|
||||
setJsonError(err.message || 'Failed to parse JSON')
|
||||
}
|
||||
}
|
||||
|
||||
const handlePasteFromClipboard = async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText()
|
||||
if (text) {
|
||||
setJsonInput(text)
|
||||
setJsonError(null)
|
||||
} else {
|
||||
alert('Clipboard is empty or does not contain text.')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Clipboard error:', err)
|
||||
alert('Could not access clipboard. Please paste manually into the textbox.')
|
||||
}
|
||||
}
|
||||
|
||||
const handleFillSampleJson = () => {
|
||||
const sample = {
|
||||
version: "1.0",
|
||||
themeTokens: {
|
||||
brandPrimary: "#A100FF",
|
||||
brandPrimaryHover: "#C23BFF",
|
||||
brandAccent: "#FF7A1A",
|
||||
brandWarm: "#FF8F45",
|
||||
brandCool: "#E7EBF5",
|
||||
brandSuccess: "#46D36A",
|
||||
brandSoft: "#D16BFF",
|
||||
surfacePage: "#0A0D14",
|
||||
surfaceNavRest: "#0D1018",
|
||||
surfaceCard: "#151A24",
|
||||
textMain: "#F5F7FF",
|
||||
mutedText: "#A7AFBF",
|
||||
linePrimary: "#00B900",
|
||||
linePrimaryHover: "#009900",
|
||||
captionText: "#7D8598",
|
||||
borderColor: "#242A38",
|
||||
surfaceMuted: "#111622",
|
||||
fontBody: "",
|
||||
fontDisplay: "",
|
||||
shadowCard: "0 2px 8px rgba(0,0,0,0.30)",
|
||||
shadowCardHover: "0 8px 24px rgba(0,0,0,0.40)",
|
||||
radiusBase: "8px",
|
||||
radiusCard: "16px"
|
||||
},
|
||||
scrollRevealEnabled: true,
|
||||
scrollRevealStyle: "fade-up",
|
||||
scrollRevealDuration: 700,
|
||||
headerStyle: "single_line"
|
||||
}
|
||||
setJsonInput(JSON.stringify(sample, null, 2))
|
||||
setJsonError(null)
|
||||
}
|
||||
|
||||
// ── File Export & Import ──────────────────────────────────────────────────
|
||||
const handleExport = () => {
|
||||
const dataStr = JSON.stringify(getCurrentThemeJson(), null, 2)
|
||||
const blob = new Blob([dataStr], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
@@ -87,6 +357,7 @@ export default function ThemeSettingsPage() {
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
URL.revokeObjectURL(url)
|
||||
showToast('Theme file exported!')
|
||||
}
|
||||
|
||||
const handleImport = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -98,52 +369,8 @@ export default function ThemeSettingsPage() {
|
||||
try {
|
||||
const content = event.target?.result as string
|
||||
const parsed = JSON.parse(content)
|
||||
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
throw new Error('Invalid JSON format: expected an object')
|
||||
}
|
||||
|
||||
// Extract tokens (support "tokens", "themeTokens", or root dictionary)
|
||||
const extractedTokens =
|
||||
(parsed.tokens && typeof parsed.tokens === 'object' && !Array.isArray(parsed.tokens)) ? parsed.tokens :
|
||||
(parsed.themeTokens && typeof parsed.themeTokens === 'object' && !Array.isArray(parsed.themeTokens)) ? parsed.themeTokens :
|
||||
(parsed.brandPrimary || parsed.surfacePage || parsed.textMain) ? parsed : null
|
||||
|
||||
if (extractedTokens) {
|
||||
setThemeTokens(cur => ({ ...cur, ...extractedTokens }))
|
||||
}
|
||||
|
||||
// Extract Scroll Reveal (support nested scrollReveal object or flat properties)
|
||||
if (parsed.scrollReveal && typeof parsed.scrollReveal === 'object') {
|
||||
if (typeof parsed.scrollReveal.enabled === 'boolean') {
|
||||
setScrollRevealEnabled(parsed.scrollReveal.enabled)
|
||||
}
|
||||
if (typeof parsed.scrollReveal.style === 'string') {
|
||||
setScrollRevealStyle(parsed.scrollReveal.style)
|
||||
}
|
||||
if (parsed.scrollReveal.duration !== undefined) {
|
||||
setScrollRevealDuration(parsed.scrollReveal.duration.toString())
|
||||
}
|
||||
} else {
|
||||
if (typeof parsed.scrollRevealEnabled === 'boolean') {
|
||||
setScrollRevealEnabled(parsed.scrollRevealEnabled)
|
||||
}
|
||||
if (typeof parsed.scrollRevealStyle === 'string') {
|
||||
setScrollRevealStyle(parsed.scrollRevealStyle)
|
||||
}
|
||||
if (parsed.scrollRevealDuration !== undefined) {
|
||||
setScrollRevealDuration(parsed.scrollRevealDuration.toString())
|
||||
}
|
||||
}
|
||||
|
||||
// Extract Header Style (support nested header object or flat headerStyle)
|
||||
if (parsed.header && typeof parsed.header === 'object' && typeof parsed.header.style === 'string') {
|
||||
setHeaderStyle(parsed.header.style)
|
||||
} else if (typeof parsed.headerStyle === 'string') {
|
||||
setHeaderStyle(parsed.headerStyle)
|
||||
}
|
||||
|
||||
alert('Theme settings loaded from JSON! Click "Save Theme Settings" to apply changes.')
|
||||
applyJsonData(parsed)
|
||||
showToast('Theme settings loaded from JSON file!')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
alert('Failed to parse JSON file. Please make sure it is a valid theme settings JSON.')
|
||||
@@ -156,6 +383,7 @@ export default function ThemeSettingsPage() {
|
||||
reader.readAsText(file)
|
||||
}
|
||||
|
||||
// ── Save to Server ────────────────────────────────────────────────────────
|
||||
const handleSave = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setSaving(true)
|
||||
@@ -171,20 +399,74 @@ export default function ThemeSettingsPage() {
|
||||
headerStyle,
|
||||
}),
|
||||
})
|
||||
if (res.ok) alert('Saved successfully!')
|
||||
} catch { alert('Failed to save') } finally { setSaving(false) }
|
||||
if (res.ok) {
|
||||
showToast('Theme settings saved successfully!')
|
||||
} else {
|
||||
alert('Failed to save settings')
|
||||
}
|
||||
} catch {
|
||||
alert('Failed to save')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
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-3xl space-y-8 animate-fade-in">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<div className="max-w-4xl 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">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-3xl font-bold">Theme & Design</h1>
|
||||
<div className="flex items-center gap-3">
|
||||
<h1 className="text-3xl font-bold text-white">Theme & Design</h1>
|
||||
|
||||
{/* Show Prompt Button next to Title */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsPromptModalOpen(true)}
|
||||
className="flex items-center gap-1.5 px-3 py-1 rounded-full bg-purple-500/10 hover:bg-purple-500/20 text-purple-300 border border-purple-500/30 text-xs font-semibold transition-all cursor-pointer shadow-sm hover:scale-105"
|
||||
title="Show LLM Prompt for ChatGPT / Claude / Gemini"
|
||||
>
|
||||
<Bot className="w-3.5 h-3.5 text-purple-400" />
|
||||
<span>Show Prompt</span>
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-slate-400 text-sm">Color tokens, typography, and scroll animation settings.</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{/* Copy JSON Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopyJson}
|
||||
className="px-3.5 py-2 rounded-xl bg-white/5 hover:bg-white/10 text-slate-300 hover:text-white border border-white/10 text-xs font-semibold flex items-center gap-1.5 transition-all cursor-pointer shadow-sm"
|
||||
title="Copy full theme settings JSON to clipboard"
|
||||
>
|
||||
<Copy className="w-4 h-4 text-purple-400" />
|
||||
<span>Copy JSON</span>
|
||||
</button>
|
||||
|
||||
{/* Paste JSON Button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setIsJsonBoxOpen(!isJsonBoxOpen)
|
||||
setJsonError(null)
|
||||
}}
|
||||
className={`px-3.5 py-2 rounded-xl text-xs font-semibold flex items-center gap-1.5 transition-all border cursor-pointer shadow-sm ${
|
||||
isJsonBoxOpen
|
||||
? 'bg-cyan-glow text-night-950 border-cyan-glow font-bold shadow-cyan-glow/20'
|
||||
: 'bg-cyan-glow/10 hover:bg-cyan-glow/20 text-cyan-glow border-cyan-glow/20'
|
||||
}`}
|
||||
title="Paste theme settings JSON"
|
||||
>
|
||||
<FileJson className="w-4 h-4" />
|
||||
<span>{isJsonBoxOpen ? 'Hide JSON Box' : 'Paste JSON'}</span>
|
||||
</button>
|
||||
|
||||
{/* Hidden File Input for Import */}
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
@@ -192,72 +474,371 @@ export default function ThemeSettingsPage() {
|
||||
accept=".json,application/json"
|
||||
className="hidden"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
className="btn-secondary py-2 px-3.5 text-xs font-semibold flex items-center gap-1.5"
|
||||
className="px-3.5 py-2 rounded-xl bg-white/5 hover:bg-white/10 text-slate-300 hover:text-white border border-white/10 text-xs font-semibold flex items-center gap-1.5 transition-all cursor-pointer shadow-sm"
|
||||
title="Import theme settings from JSON file"
|
||||
>
|
||||
<Upload className="w-4 h-4 text-cyan-glow" />
|
||||
Import JSON
|
||||
<span>Import File</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleExport}
|
||||
className="btn-secondary py-2 px-3.5 text-xs font-semibold flex items-center gap-1.5"
|
||||
className="px-3.5 py-2 rounded-xl bg-white/5 hover:bg-white/10 text-slate-300 hover:text-white border border-white/10 text-xs font-semibold flex items-center gap-1.5 transition-all cursor-pointer shadow-sm"
|
||||
title="Export theme settings to JSON file"
|
||||
>
|
||||
<Download className="w-4 h-4 text-slate-300" />
|
||||
Export JSON
|
||||
<Download className="w-4 h-4 text-slate-400" />
|
||||
<span>Export File</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toast Notification */}
|
||||
{toastMessage && (
|
||||
<div className="p-4 rounded-2xl bg-emerald-500/10 border border-emerald-500/20 text-emerald-400 flex items-center gap-3 animate-fade-in shadow-lg">
|
||||
<CheckCircle2 className="w-5 h-5 flex-shrink-0" />
|
||||
<span className="text-sm font-semibold">{toastMessage}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Expandable JSON Paste Box */}
|
||||
{isJsonBoxOpen && (
|
||||
<div className="admin-card p-6 border-cyan-glow/30 bg-night-950/80 shadow-2xl space-y-4 animate-fade-in">
|
||||
<div className="flex items-center justify-between border-b border-white/5 pb-3">
|
||||
<div className="flex items-center gap-2 text-sm font-bold text-cyan-glow">
|
||||
<Code2 className="w-4 h-4" />
|
||||
<span>Paste Theme Settings JSON</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePasteFromClipboard}
|
||||
className="px-2.5 py-1 rounded-lg bg-white/5 hover:bg-white/10 text-[11px] text-slate-300 hover:text-white flex items-center gap-1 cursor-pointer transition-colors"
|
||||
title="Paste text directly from clipboard"
|
||||
>
|
||||
<ClipboardPaste className="w-3.5 h-3.5 text-cyan-glow" />
|
||||
<span>Paste Clipboard</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleFillSampleJson}
|
||||
className="px-2.5 py-1 rounded-lg bg-white/5 hover:bg-white/10 text-[11px] text-slate-300 hover:text-white flex items-center gap-1 cursor-pointer transition-colors"
|
||||
title="Insert sample JSON template"
|
||||
>
|
||||
<Sparkles className="w-3.5 h-3.5 text-amber-400" />
|
||||
<span>Sample Template</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsJsonBoxOpen(false)}
|
||||
className="p-1 rounded-lg hover:bg-white/10 text-slate-500 hover:text-white cursor-pointer"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
rows={8}
|
||||
value={jsonInput}
|
||||
onChange={(e) => {
|
||||
setJsonInput(e.target.value)
|
||||
setJsonError(null)
|
||||
}}
|
||||
className="w-full font-mono text-xs p-4 bg-night-900 border border-white/10 focus:border-cyan-glow/40 rounded-xl outline-none text-cyan-200 placeholder:text-slate-600 resize-y shadow-inner"
|
||||
placeholder='{\n "version": "1.0",\n "themeTokens": {\n "brandPrimary": "#A100FF",\n "surfacePage": "#0A0D14",\n "textMain": "#F5F7FF"\n }\n}'
|
||||
/>
|
||||
|
||||
{jsonError && (
|
||||
<div className="p-3 rounded-xl bg-rose-500/10 border border-rose-500/20 text-rose-400 text-xs flex items-center gap-2">
|
||||
<AlertCircle className="w-4 h-4 flex-shrink-0" />
|
||||
<span>{jsonError}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between pt-1">
|
||||
<p className="text-[11px] text-slate-500">
|
||||
Supports <code className="text-slate-400">themeTokens</code>, animation settings, and header style.
|
||||
</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsJsonBoxOpen(false)}
|
||||
className="px-4 py-2 rounded-xl bg-white/5 hover:bg-white/10 text-slate-400 text-xs font-semibold cursor-pointer"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleApplyJsonText}
|
||||
className="btn-primary px-5 py-2 text-xs font-bold flex items-center gap-1.5 cursor-pointer shadow-md shadow-cyan-glow/20"
|
||||
>
|
||||
<Check className="w-4 h-4" />
|
||||
<span>Apply to Theme</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Show Prompt Modal ── */}
|
||||
{isPromptModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-night-950/80 backdrop-blur-md animate-fade-in overflow-y-auto">
|
||||
<div className="w-full max-w-3xl bg-night-900 border border-white/10 rounded-3xl shadow-2xl flex flex-col overflow-hidden my-8">
|
||||
|
||||
{/* Modal Header */}
|
||||
<div className="p-6 border-b border-white/5 flex items-center justify-between bg-white/[0.02]">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2.5 rounded-xl bg-purple-500/10 border border-purple-500/20 text-purple-400">
|
||||
<Bot className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-white">LLM Theme Generator Prompt</h3>
|
||||
<p className="text-xs text-slate-400">Copy this prompt to ChatGPT, Claude, or Gemini to create a matching theme</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCopyPrompt}
|
||||
className="btn-primary px-4 py-2 text-xs font-bold flex items-center gap-1.5 cursor-pointer shadow-lg shadow-cyan-glow/10"
|
||||
>
|
||||
<Copy className="w-3.5 h-3.5" />
|
||||
<span>Copy Prompt</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setIsPromptModalOpen(false)}
|
||||
className="p-2 rounded-xl hover:bg-white/5 text-slate-400 hover:text-white transition-all cursor-pointer"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal Body */}
|
||||
<div className="p-6 space-y-4 max-h-[65vh] overflow-y-auto">
|
||||
<div className="p-4 rounded-xl bg-purple-500/5 border border-purple-500/10 text-purple-300 text-xs leading-relaxed">
|
||||
💡 <strong>เคล็ดลับ:</strong> เมื่อก๊อปปี้ข้อความด้านล่างไปวางใน LLM สามารถต่อท้ายคำสั่งด้วยโจทย์เฉพาะได้ เช่น <em>"ขอธีมโทน Rose Gold สำหรับร้านเครื่องสำอางผู้หญิง"</em> หรือ <em>"ขอธีม Minimalist Dark Mode สไตล์ญี่ปุ่น"</em> แล้วนำผลลัพธ์ JSON กลับมากด <strong>Paste JSON</strong> ได้ทันที
|
||||
</div>
|
||||
|
||||
<div className="relative group">
|
||||
<pre className="p-4 rounded-2xl bg-night-950 border border-white/10 font-mono text-xs text-slate-300 leading-relaxed whitespace-pre-wrap select-all overflow-x-auto shadow-inner">
|
||||
{LLM_PROMPT_TEXT}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Modal Footer */}
|
||||
<div className="p-4 px-6 border-t border-white/5 bg-white/[0.01] flex items-center justify-between text-xs text-slate-400">
|
||||
<span>Template: theme-settings-2026-09-22.json compatible</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsPromptModalOpen(false)}
|
||||
className="px-4 py-2 rounded-xl bg-white/5 hover:bg-white/10 text-slate-300 hover:text-white font-semibold cursor-pointer"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Settings Form */}
|
||||
<form onSubmit={handleSave} className="space-y-8">
|
||||
{/* Theme Tokens */}
|
||||
<div className="admin-card p-8 space-y-6">
|
||||
|
||||
{/* ── 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-lg"><span className="text-xl">🎨</span></div>
|
||||
<h2 className="text-lg font-bold uppercase tracking-widest text-white">Theme Tokens</h2>
|
||||
<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>
|
||||
</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">
|
||||
{[
|
||||
{ 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>
|
||||
</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="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>
|
||||
</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="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>
|
||||
</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>
|
||||
</div>
|
||||
|
||||
{/* ── 2. Typography & Font Presets ── */}
|
||||
<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-purple-500/10 rounded-xl text-purple-400">
|
||||
<span className="text-xl">🔤</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-white">Typography & Fonts</h2>
|
||||
<p className="text-xs text-slate-400">Select standard font pairings or define custom font families</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs uppercase font-bold tracking-widest text-slate-400">Font Preset</label>
|
||||
<select value={fontPreset} onChange={e => applyFontPreset(e.target.value as FontPresetKey | 'custom')}
|
||||
className="admin-input w-full max-w-sm bg-slate-900 border-white/10">
|
||||
{Object.entries(FONT_PRESETS).map(([key, p]) => <option key={key} value={key}>{p.label}</option>)}
|
||||
<select
|
||||
value={fontPreset}
|
||||
onChange={e => applyFontPreset(e.target.value as FontPresetKey | 'custom')}
|
||||
className="admin-input w-full max-w-sm bg-night-900 border-white/10"
|
||||
>
|
||||
{Object.entries(FONT_PRESETS).map(([key, p]) => (
|
||||
<option key={key} value={key}>{p.label}</option>
|
||||
))}
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{(['brandPrimary', 'brandPrimaryHover', 'brandAccent', 'surfacePage', 'surfaceCard', 'textMain', 'mutedText', 'linePrimary', 'linePrimaryHover', 'captionText', 'borderColor', 'surfaceMuted'] as const).map(key => (
|
||||
<div key={key} className="space-y-2">
|
||||
<label className="text-xs uppercase font-bold tracking-widest text-slate-400">{key.replace(/([A-Z])/g, ' $1').trim()}</label>
|
||||
<input type="color" value={themeTokens[key] || '#000000'}
|
||||
onChange={e => setThemeTokens(cur => ({ ...cur, [key]: e.target.value }))}
|
||||
className="h-12 w-full max-w-sm rounded-xl border border-white/10 bg-slate-900 p-1" />
|
||||
</div>
|
||||
))}
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs uppercase font-bold tracking-widest text-slate-400">Body Font Family</label>
|
||||
<input
|
||||
type="text"
|
||||
value={themeTokens.fontBody || ''}
|
||||
onChange={e => setThemeTokens(cur => ({ ...cur, fontBody: e.target.value }))}
|
||||
className="admin-input w-full text-xs font-mono"
|
||||
placeholder="Leave blank to use locale preset"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs uppercase font-bold tracking-widest text-slate-400">Display / Heading Font Family</label>
|
||||
<input
|
||||
type="text"
|
||||
value={themeTokens.fontDisplay || ''}
|
||||
onChange={e => setThemeTokens(cur => ({ ...cur, fontDisplay: e.target.value }))}
|
||||
className="admin-input w-full text-xs font-mono"
|
||||
placeholder="Leave blank to use locale preset"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── 3. Geometry & Shadows ── */}
|
||||
<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-blue-500/10 rounded-xl text-blue-400">
|
||||
<span className="text-xl">📐</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold text-white">Corner Radiuses & Shadows</h2>
|
||||
<p className="text-xs text-slate-400">Card elevation shadows and border radiuses</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{(['fontBody', 'fontDisplay', 'shadowCard', 'shadowCardHover', 'radiusBase', 'radiusCard'] as const).map(key => (
|
||||
{[
|
||||
{ key: 'radiusBase', label: 'Radius Base (Buttons/Inputs)', placeholder: 'e.g. 8px' },
|
||||
{ key: 'radiusCard', label: 'Radius Card', placeholder: 'e.g. 16px' },
|
||||
{ key: 'shadowCard', label: 'Shadow Card', placeholder: 'e.g. 0 2px 8px rgba(0,0,0,0.30)' },
|
||||
{ key: 'shadowCardHover', label: 'Shadow Card Hover', placeholder: 'e.g. 0 8px 24px rgba(0,0,0,0.40)' },
|
||||
].map(({ key, label, placeholder }) => (
|
||||
<div key={key} className="space-y-2">
|
||||
<label className="text-xs uppercase font-bold tracking-widest text-slate-400">{key.replace(/([A-Z])/g, ' $1').trim()}</label>
|
||||
<input type="text" value={themeTokens[key] || ''}
|
||||
<label className="text-xs uppercase font-bold tracking-widest text-slate-400">{label}</label>
|
||||
<input
|
||||
type="text"
|
||||
value={themeTokens[key] || ''}
|
||||
onChange={e => setThemeTokens(cur => ({ ...cur, [key]: e.target.value }))}
|
||||
className="admin-input w-full"
|
||||
placeholder={key.startsWith('font') ? 'Leave blank to use locale preset' : key.startsWith('radius') ? 'e.g. 8px' : undefined} />
|
||||
className="admin-input w-full text-xs font-mono"
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Header Layout Style */}
|
||||
<div className="admin-card p-8 space-y-6">
|
||||
{/* ── 4. Header Layout Style ── */}
|
||||
<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-amber-500/10 rounded-lg"><span className="text-xl">🧭</span></div>
|
||||
<div className="p-2 bg-amber-500/10 rounded-xl text-amber-400">
|
||||
<span className="text-xl">🧭</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold uppercase tracking-widest text-white">PC Header Layout Style</h2>
|
||||
<h2 className="text-lg font-bold text-white">PC Header Layout Style</h2>
|
||||
<p className="text-xs text-slate-400 mt-0.5">เลือกรูปแบบการจัดวางเมนูส่วนหัวหลักสำหรับหน้าจอแนวกว้าง (PC / Desktop View)</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -266,9 +847,9 @@ export default function ThemeSettingsPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setHeaderStyle('single_line')}
|
||||
className={`flex flex-col gap-3 rounded-2xl border p-5 text-left transition ${
|
||||
className={`flex flex-col gap-3 rounded-2xl border p-5 text-left transition cursor-pointer ${
|
||||
headerStyle === 'single_line'
|
||||
? 'border-amber-500 bg-amber-500/10 text-white'
|
||||
? 'border-amber-500 bg-amber-500/10 text-white shadow-lg shadow-amber-500/10'
|
||||
: 'border-white/10 bg-white/5 text-slate-400 hover:border-white/20'
|
||||
}`}
|
||||
>
|
||||
@@ -287,9 +868,9 @@ export default function ThemeSettingsPage() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setHeaderStyle('two_tier')}
|
||||
className={`flex flex-col gap-3 rounded-2xl border p-5 text-left transition ${
|
||||
className={`flex flex-col gap-3 rounded-2xl border p-5 text-left transition cursor-pointer ${
|
||||
headerStyle === 'two_tier'
|
||||
? 'border-amber-500 bg-amber-500/10 text-white'
|
||||
? 'border-amber-500 bg-amber-500/10 text-white shadow-lg shadow-amber-500/10'
|
||||
: 'border-white/10 bg-white/5 text-slate-400 hover:border-white/20'
|
||||
}`}
|
||||
>
|
||||
@@ -312,12 +893,14 @@ export default function ThemeSettingsPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scroll Reveal */}
|
||||
<div className="admin-card p-8 space-y-6">
|
||||
{/* ── 5. Scroll Reveal Animation ── */}
|
||||
<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-violet-500/10 rounded-lg"><Sparkles className="w-5 h-5 text-violet-400" /></div>
|
||||
<div className="p-2 bg-violet-500/10 rounded-xl text-violet-400">
|
||||
<Sparkles className="w-5 h-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-lg font-bold uppercase tracking-widest text-white">Scroll Reveal Animation</h2>
|
||||
<h2 className="text-lg font-bold text-white">Scroll Reveal Animation</h2>
|
||||
<p className="text-xs text-slate-400 mt-0.5">ควบคุม animation effect ที่แสดงเมื่อ scroll ลงมาในหน้าเว็บ</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -327,8 +910,11 @@ export default function ThemeSettingsPage() {
|
||||
<p className="text-sm font-semibold text-white">Enable Scroll Reveal</p>
|
||||
<p className="text-xs text-slate-400 mt-0.5">เปิดหรือปิด animation ทั้งหมดในเว็บไซต์</p>
|
||||
</div>
|
||||
<button type="button" onClick={() => setScrollRevealEnabled(v => !v)}
|
||||
className={`relative inline-flex h-7 w-12 items-center rounded-full transition-colors ${scrollRevealEnabled ? 'bg-violet-500' : 'bg-slate-600'}`}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setScrollRevealEnabled(v => !v)}
|
||||
className={`relative inline-flex h-7 w-12 items-center rounded-full transition-colors cursor-pointer ${scrollRevealEnabled ? 'bg-violet-500' : 'bg-slate-600'}`}
|
||||
>
|
||||
<span className={`inline-block h-5 w-5 transform rounded-full bg-white shadow transition-transform ${scrollRevealEnabled ? 'translate-x-6' : 'translate-x-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
@@ -345,9 +931,14 @@ export default function ThemeSettingsPage() {
|
||||
{ value: 'zoom-in', label: 'Zoom In', icon: '⊕' },
|
||||
{ value: 'none', label: 'None', icon: '—' },
|
||||
].map(opt => (
|
||||
<button key={opt.value} type="button" onClick={() => setScrollRevealStyle(opt.value)}
|
||||
className={`flex flex-col items-center gap-1 rounded-lg border py-3 px-2 text-xs font-semibold transition ${scrollRevealStyle === opt.value ? 'border-violet-500 bg-violet-500/20 text-violet-300' : 'border-white/10 bg-white/5 text-slate-400 hover:border-white/20'}`}>
|
||||
<span className="text-lg">{opt.icon}</span>{opt.label}
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
onClick={() => setScrollRevealStyle(opt.value)}
|
||||
className={`flex flex-col items-center gap-1 rounded-xl border py-3 px-2 text-xs font-semibold transition cursor-pointer ${scrollRevealStyle === opt.value ? 'border-violet-500 bg-violet-500/20 text-violet-300 shadow-md shadow-violet-500/10' : 'border-white/10 bg-white/5 text-slate-400 hover:border-white/20'}`}
|
||||
>
|
||||
<span className="text-lg">{opt.icon}</span>
|
||||
<span>{opt.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -356,23 +947,37 @@ export default function ThemeSettingsPage() {
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-xs uppercase font-bold tracking-widest text-slate-400">Animation Duration</label>
|
||||
<span className="text-sm font-bold text-violet-300">{scrollRevealDuration} ms</span>
|
||||
<span className="text-sm font-bold text-violet-300 font-mono">{scrollRevealDuration} ms</span>
|
||||
</div>
|
||||
<input type="range" min="200" max="1500" step="50" value={scrollRevealDuration}
|
||||
onChange={e => setScrollRevealDuration(e.target.value)} className="w-full accent-violet-500" />
|
||||
<input
|
||||
type="range"
|
||||
min="200"
|
||||
max="1500"
|
||||
step="50"
|
||||
value={scrollRevealDuration}
|
||||
onChange={e => setScrollRevealDuration(e.target.value)}
|
||||
className="w-full accent-violet-500 cursor-pointer"
|
||||
/>
|
||||
<div className="flex justify-between text-[10px] text-slate-500">
|
||||
<span>เร็ว (200ms)</span><span>ช้า (1500ms)</span>
|
||||
<span>เร็ว (200ms)</span>
|
||||
<span>ช้า (1500ms)</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button type="submit" disabled={saving}
|
||||
className="btn-primary py-4 px-12 flex items-center gap-2 ml-auto text-sm">
|
||||
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
||||
Save Theme Settings
|
||||
</button>
|
||||
{/* Submit Button */}
|
||||
<div className="flex justify-end pt-4">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={saving}
|
||||
className="btn-primary py-4 px-12 flex items-center gap-2 text-sm font-bold shadow-xl shadow-cyan-glow/10 cursor-pointer"
|
||||
>
|
||||
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
|
||||
<span>Save Theme Settings</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user