diff --git a/LLM_PROMPT_THEME_GENERATOR.md b/LLM_PROMPT_THEME_GENERATOR.md new file mode 100644 index 0000000..6bb9462 --- /dev/null +++ b/LLM_PROMPT_THEME_GENERATOR.md @@ -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"** เพื่อบันทึกใช้งานจริงทันที diff --git a/apps/admin/src/pages/ThemeSettingsPage.tsx b/apps/admin/src/pages/ThemeSettingsPage.tsx index 183b1d4..16c1b28 100644 --- a/apps/admin/src/pages/ThemeSettingsPage.tsx +++ b/apps/admin/src/pages/ThemeSettingsPage.tsx @@ -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) { 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(null) + const [toastMessage, setToastMessage] = useState(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) => { @@ -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
return ( -
-
+
+ + {/* Top Header & Action Buttons */} +
-

Theme & Design

+
+

Theme & Design

+ + {/* Show Prompt Button next to Title */} + +

Color tokens, typography, and scroll animation settings.

-
+ +
+ {/* Copy JSON Button */} + + + {/* Paste JSON Button */} + + + {/* Hidden File Input for Import */} + +
+ {/* Toast Notification */} + {toastMessage && ( +
+ + {toastMessage} +
+ )} + + {/* Expandable JSON Paste Box */} + {isJsonBoxOpen && ( +
+
+
+ + Paste Theme Settings JSON +
+ +
+ + + +
+
+ +