From c2269ea090b982cf5787cf65c919e7d1c80f0923 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 23 Sep 2026 06:08:36 +0700 Subject: [PATCH] x --- apps/admin/src/pages/ThemeSettingsPage.tsx | 127 ++++++++++++++++++++- theme-settings-2026-09-22.json | 40 +++++++ 2 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 theme-settings-2026-09-22.json diff --git a/apps/admin/src/pages/ThemeSettingsPage.tsx b/apps/admin/src/pages/ThemeSettingsPage.tsx index 3b7ce67..183b1d4 100644 --- a/apps/admin/src/pages/ThemeSettingsPage.tsx +++ b/apps/admin/src/pages/ThemeSettingsPage.tsx @@ -1,6 +1,6 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import { API_BASE, type StoreSettings } from '@homaroma/types' -import { Save, Loader2, Sparkles } from 'lucide-react' +import { Save, Loader2, Sparkles, Download, Upload } from 'lucide-react' const defaultThemeTokens = { brandPrimary: '#FF6B6B', brandPrimaryHover: '#FF4500', brandAccent: '#FFF200', @@ -38,6 +38,7 @@ function resolveFontPreset(tokens: Record) { export default function ThemeSettingsPage() { const [loading, setLoading] = useState(true) const [saving, setSaving] = useState(false) + const fileInputRef = useRef(null) const [themeTokens, setThemeTokens] = useState>(defaultThemeTokens) const [scrollRevealEnabled, setScrollRevealEnabled] = useState(true) @@ -66,6 +67,95 @@ export default function ThemeSettingsPage() { .finally(() => setLoading(false)) }, []) + const handleExport = () => { + const data = { + version: '1.0', + exportedAt: new Date().toISOString(), + themeTokens, + scrollRevealEnabled, + scrollRevealStyle, + scrollRevealDuration: parseInt(scrollRevealDuration) || 700, + headerStyle, + } + const dataStr = JSON.stringify(data, null, 2) + const blob = new Blob([dataStr], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const link = document.createElement('a') + link.href = url + link.download = `theme-settings-${new Date().toISOString().split('T')[0]}.json` + document.body.appendChild(link) + link.click() + document.body.removeChild(link) + URL.revokeObjectURL(url) + } + + const handleImport = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + + const reader = new FileReader() + reader.onload = (event) => { + 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.') + } catch (err) { + console.error(err) + alert('Failed to parse JSON file. Please make sure it is a valid theme settings JSON.') + } finally { + if (fileInputRef.current) { + fileInputRef.current.value = '' + } + } + } + reader.readAsText(file) + } + const handleSave = async (e: React.FormEvent) => { e.preventDefault() setSaving(true) @@ -89,9 +179,36 @@ export default function ThemeSettingsPage() { return (
-
-

Theme & Design

-

Color tokens, typography, and scroll animation settings.

+
+
+

Theme & Design

+

Color tokens, typography, and scroll animation settings.

+
+
+ + + +
diff --git a/theme-settings-2026-09-22.json b/theme-settings-2026-09-22.json new file mode 100644 index 0000000..cb15aba --- /dev/null +++ b/theme-settings-2026-09-22.json @@ -0,0 +1,40 @@ +{ + "version": "1.0", + "themeTokens": { + "brandPrimary": "#B8956A", + "brandPrimaryHover": "#A07D3A", + "brandAccent": "#D4B97A", + "brandWarm": "#D4785A", + "brandCool": "#EDE6DD", + "brandSuccess": "#5B9A4D", + "brandSoft": "#DABE7E", + + "surfacePage": "#F5F0EB", + "surfaceNavRest": "#14171C", + "surfaceCard": "#FFFFFF", + + "textMain": "#EDE7E1", + "mutedText": "#BDB7B0", + + "linePrimary": "#B8956A", + "linePrimaryHover": "#D4B97A", + + "captionText": "#A9A39D", + "borderColor": "#2A2E34", + "surfaceMuted": "#1B1F24", + + "fontBody": "", + "fontDisplay": "", + + "shadowCard": "0 1px 4px rgba(0,0,0,0.12)", + "shadowCardHover": "0 4px 12px rgba(0,0,0,0.18)", + + "radiusBase": "8px", + "radiusCard": "16px" + }, + + "scrollRevealEnabled": true, + "scrollRevealStyle": "fade-up", + "scrollRevealDuration": 700, + "headerStyle": "single_line" +} \ No newline at end of file