x
This commit is contained in:
1 parent
f9c4046d35
commit
c365add07f
3 files changed
+120
-9
No files matched your search
@@ -17,12 +17,46 @@ const defaultThemeTokens = {
|
|||||||
mutedText: '#666666',
|
mutedText: '#666666',
|
||||||
linePrimary: '#00B900',
|
linePrimary: '#00B900',
|
||||||
linePrimaryHover: '#009900',
|
linePrimaryHover: '#009900',
|
||||||
fontBody: 'system-ui, sans-serif',
|
fontBody: '',
|
||||||
fontDisplay: 'system-ui, sans-serif',
|
fontDisplay: '',
|
||||||
shadowCard: '0 10px 25px rgba(0,0,0,0.05)',
|
shadowCard: '0 10px 25px rgba(0,0,0,0.05)',
|
||||||
shadowCardHover: '0 20px 40px rgba(0,0,0,0.1)',
|
shadowCardHover: '0 20px 40px rgba(0,0,0,0.1)',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FONT_PRESETS = {
|
||||||
|
locale: {
|
||||||
|
label: 'Locale default',
|
||||||
|
body: '',
|
||||||
|
display: '',
|
||||||
|
},
|
||||||
|
sarabun: {
|
||||||
|
label: 'Sarabun',
|
||||||
|
body: 'var(--font-sarabun, "Sarabun"), "Noto Sans Thai", ui-sans-serif, system-ui, sans-serif',
|
||||||
|
display: 'var(--font-sarabun, "Sarabun"), "Noto Sans Thai", ui-sans-serif, system-ui, sans-serif',
|
||||||
|
},
|
||||||
|
kanit: {
|
||||||
|
label: 'Kanit',
|
||||||
|
body: 'var(--font-kanit, "Kanit"), "Noto Sans Thai", ui-sans-serif, system-ui, sans-serif',
|
||||||
|
display: 'var(--font-kanit, "Kanit"), "Noto Sans Thai", ui-sans-serif, system-ui, sans-serif',
|
||||||
|
},
|
||||||
|
} as const
|
||||||
|
|
||||||
|
type FontPresetKey = keyof typeof FONT_PRESETS
|
||||||
|
|
||||||
|
function resolveFontPreset(themeTokens: Record<string, string>) {
|
||||||
|
const body = themeTokens.fontBody?.trim() || ''
|
||||||
|
const display = themeTokens.fontDisplay?.trim() || ''
|
||||||
|
const fontBody = body || display
|
||||||
|
const fontDisplay = display || body
|
||||||
|
|
||||||
|
if (!fontBody && !fontDisplay) return 'locale'
|
||||||
|
|
||||||
|
if (fontBody === FONT_PRESETS.sarabun.body && fontDisplay === FONT_PRESETS.sarabun.display) return 'sarabun'
|
||||||
|
if (fontBody === FONT_PRESETS.kanit.body && fontDisplay === FONT_PRESETS.kanit.display) return 'kanit'
|
||||||
|
|
||||||
|
return 'custom'
|
||||||
|
}
|
||||||
|
|
||||||
export default function SystemSettingsPage() {
|
export default function SystemSettingsPage() {
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
@@ -49,6 +83,17 @@ export default function SystemSettingsPage() {
|
|||||||
const [footerSupportHeading, setFooterSupportHeading] = useState('')
|
const [footerSupportHeading, setFooterSupportHeading] = useState('')
|
||||||
const [footerCopyrightText, setFooterCopyrightText] = useState('')
|
const [footerCopyrightText, setFooterCopyrightText] = useState('')
|
||||||
const [themeTokens, setThemeTokens] = useState<Record<string, string>>(defaultThemeTokens)
|
const [themeTokens, setThemeTokens] = useState<Record<string, string>>(defaultThemeTokens)
|
||||||
|
const fontPreset = resolveFontPreset(themeTokens)
|
||||||
|
|
||||||
|
const applyFontPreset = (preset: FontPresetKey | 'custom') => {
|
||||||
|
if (preset === 'custom') return
|
||||||
|
|
||||||
|
setThemeTokens((current) => ({
|
||||||
|
...current,
|
||||||
|
fontBody: FONT_PRESETS[preset].body,
|
||||||
|
fontDisplay: FONT_PRESETS[preset].display,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch(`${API_BASE}/settings`)
|
fetch(`${API_BASE}/settings`)
|
||||||
@@ -365,6 +410,25 @@ export default function SystemSettingsPage() {
|
|||||||
<h2 className="text-lg font-bold uppercase tracking-widest text-white">Theme Tokens</h2>
|
<h2 className="text-lg font-bold uppercase tracking-widest text-white">Theme Tokens</h2>
|
||||||
</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, preset]) => (
|
||||||
|
<option key={key} value={key}>
|
||||||
|
{preset.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
<option value="custom">Custom</option>
|
||||||
|
</select>
|
||||||
|
<p className="text-[10px] text-slate-400">
|
||||||
|
Select a ready-made font stack, or use custom overrides below when you need a special brand voice.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
{[
|
{[
|
||||||
['brandPrimary', 'Brand Primary'],
|
['brandPrimary', 'Brand Primary'],
|
||||||
@@ -391,8 +455,8 @@ export default function SystemSettingsPage() {
|
|||||||
|
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
{[
|
{[
|
||||||
['fontBody', 'Font Body'],
|
['fontBody', 'Font Body Override'],
|
||||||
['fontDisplay', 'Font Display'],
|
['fontDisplay', 'Font Display Override'],
|
||||||
['shadowCard', 'Shadow Card'],
|
['shadowCard', 'Shadow Card'],
|
||||||
['shadowCardHover', 'Shadow Card Hover'],
|
['shadowCardHover', 'Shadow Card Hover'],
|
||||||
].map(([key, label]) => (
|
].map(([key, label]) => (
|
||||||
@@ -403,6 +467,7 @@ export default function SystemSettingsPage() {
|
|||||||
value={themeTokens[key] || ''}
|
value={themeTokens[key] || ''}
|
||||||
onChange={e => setThemeTokens((current) => ({ ...current, [key]: e.target.value }))}
|
onChange={e => setThemeTokens((current) => ({ ...current, [key]: e.target.value }))}
|
||||||
className="admin-input w-full"
|
className="admin-input w-full"
|
||||||
|
placeholder={key.startsWith('font') ? 'Leave blank to use locale preset' : undefined}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import { Kanit, Sarabun } from 'next/font/google';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import '../globals.css';
|
import '../globals.css';
|
||||||
import Navbar from '@/components/Navbar';
|
import Navbar from '@/components/Navbar';
|
||||||
@@ -10,6 +11,20 @@ import type { MenuItem, StoreSettings } from '@homaroma/types';
|
|||||||
import { isSupportedLocale } from '@/lib/locales';
|
import { isSupportedLocale } from '@/lib/locales';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
|
|
||||||
|
const sarabun = Sarabun({
|
||||||
|
subsets: ['latin', 'thai'],
|
||||||
|
display: 'swap',
|
||||||
|
weight: ['300', '400', '500', '600', '700'],
|
||||||
|
variable: '--font-sarabun',
|
||||||
|
});
|
||||||
|
|
||||||
|
const kanit = Kanit({
|
||||||
|
subsets: ['latin', 'thai'],
|
||||||
|
display: 'swap',
|
||||||
|
weight: ['300', '400', '500', '600', '700'],
|
||||||
|
variable: '--font-kanit',
|
||||||
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'HomAroma',
|
title: 'HomAroma',
|
||||||
description: 'Pure Origins, Artisan Craft, Zero Compromise',
|
description: 'Pure Origins, Artisan Craft, Zero Compromise',
|
||||||
@@ -96,8 +111,8 @@ function buildThemeStyle(settings: StoreSettings | null): CSSProperties | undefi
|
|||||||
['--color-muted-text', tokens.mutedText],
|
['--color-muted-text', tokens.mutedText],
|
||||||
['--color-line-primary', tokens.linePrimary],
|
['--color-line-primary', tokens.linePrimary],
|
||||||
['--color-line-primary-hover', tokens.linePrimaryHover],
|
['--color-line-primary-hover', tokens.linePrimaryHover],
|
||||||
['--font-body', tokens.fontBody],
|
['--font-body-active', tokens.fontBody],
|
||||||
['--font-display', tokens.fontDisplay],
|
['--font-display-active', tokens.fontDisplay],
|
||||||
['--shadow-card', tokens.shadowCard],
|
['--shadow-card', tokens.shadowCard],
|
||||||
['--shadow-card-hover', tokens.shadowCardHover],
|
['--shadow-card-hover', tokens.shadowCardHover],
|
||||||
]
|
]
|
||||||
@@ -134,7 +149,7 @@ export default async function RootLayout({
|
|||||||
const themeStyle = buildThemeStyle(settings)
|
const themeStyle = buildThemeStyle(settings)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={lang} className="scroll-smooth">
|
<html lang={lang} className={`${sarabun.variable} ${kanit.variable} scroll-smooth`}>
|
||||||
<body className="min-h-screen bg-surface-page text-text-main font-sans antialiased" style={themeStyle}>
|
<body className="min-h-screen bg-surface-page text-text-main font-sans antialiased" style={themeStyle}>
|
||||||
<ClientProviders>
|
<ClientProviders>
|
||||||
<Navbar categories={categories} initialMenu={initialMenu} settings={settings} />
|
<Navbar categories={categories} initialMenu={initialMenu} settings={settings} />
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-body: var(--font-sarabun, ui-sans-serif), system-ui, sans-serif;
|
--font-body: var(--font-body-active, var(--font-body-locale, ui-sans-serif)), system-ui, sans-serif;
|
||||||
--font-display: var(--font-body);
|
--font-display: var(--font-display-active, var(--font-display-locale, var(--font-body-locale, ui-sans-serif))), system-ui, sans-serif;
|
||||||
--font-sans: var(--font-body);
|
--font-sans: var(--font-body);
|
||||||
|
|
||||||
--color-brand-primary: #FF6B6B;
|
--color-brand-primary: #FF6B6B;
|
||||||
@@ -45,6 +45,37 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
|
:root,
|
||||||
|
html {
|
||||||
|
--font-body-locale: var(--font-sarabun, "Sarabun"), "Noto Sans Thai", ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-display-locale: var(--font-body-locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[lang="en"] {
|
||||||
|
--font-body-locale: "Inter", "Segoe UI", ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-display-locale: var(--font-body-locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[lang="ja"] {
|
||||||
|
--font-body-locale: "Hiragino Sans", "Yu Gothic", "Noto Sans JP", ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-display-locale: var(--font-body-locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[lang="zh"] {
|
||||||
|
--font-body-locale: "PingFang SC", "Microsoft YaHei", "Noto Sans SC", ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-display-locale: var(--font-body-locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[lang="ko"] {
|
||||||
|
--font-body-locale: "Apple SD Gothic Neo", "Malgun Gothic", "Noto Sans KR", ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-display-locale: var(--font-body-locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
html[lang="vi"] {
|
||||||
|
--font-body-locale: "Inter", "Segoe UI", ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-display-locale: var(--font-body-locale);
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-surface-page text-text-main font-sans antialiased;
|
@apply bg-surface-page text-text-main font-sans antialiased;
|
||||||
background-image: radial-gradient(circle at top right, rgba(255,107,107,0.15), transparent 40%),
|
background-image: radial-gradient(circle at top right, rgba(255,107,107,0.15), transparent 40%),
|
||||||
|
|||||||
Reference in new issue
Block a user