DesignEditor.tsx 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458
  1. import * as React from 'react';
  2. import { DesignSettings, ThemeName, ButtonStyle, ButtonShape, FontFamily, BackgroundType, BannerType, MediaSource, SideNavSettings, BannerSettings } from '../types';
  3. import { Icon } from './ui/Icon';
  4. import { useTranslation } from '../hooks/useI18n';
  5. interface AccordionProps {
  6. title: string;
  7. children?: React.ReactNode;
  8. defaultOpen?: boolean;
  9. }
  10. const Accordion: React.FC<AccordionProps> = ({ title, children, defaultOpen = true }) => {
  11. const [isOpen, setIsOpen] = React.useState(defaultOpen);
  12. return (
  13. <div className="border-b border-gray-200 dark:border-gray-700">
  14. <button onMouseDown={(e) => e.preventDefault()} onClick={() => setIsOpen(!isOpen)} className="w-full flex justify-between items-center py-4 text-left">
  15. <h3 className="text-lg font-semibold text-gray-900 dark:text-white">{title}</h3>
  16. <Icon className={`h-5 w-5 transition-transform text-gray-500 dark:text-gray-400 ${isOpen ? 'rotate-180' : ''}`}><path d="M19 9l-7 7-7-7" /></Icon>
  17. </button>
  18. {isOpen && <div className="pb-6 space-y-4">{children}</div>}
  19. </div>
  20. );
  21. };
  22. const ColorPalettePicker: React.FC<{
  23. label: string;
  24. color: string;
  25. onChange: (color: string) => void;
  26. themeColors?: string[];
  27. }> = ({ label, color, onChange, themeColors = ['#ffffff', '#f3f4f6', '#d1d5db', '#6b7280', '#374151', '#111827', '#10b981', '#3b82f6', '#ef4444'] }) => (
  28. <div className="flex items-center justify-between">
  29. <label className="text-sm text-gray-600 dark:text-gray-300">{label}</label>
  30. <div className="flex items-center gap-2">
  31. <div className="flex gap-1">
  32. {themeColors.map(c => (
  33. <button key={c} onMouseDown={(e) => e.preventDefault()} onClick={() => onChange(c)} className={`h-6 w-6 rounded-full border border-gray-300 dark:border-gray-600 ${color.toLowerCase() === c.toLowerCase() ? 'ring-2 ring-brand-primary ring-offset-2 ring-offset-gray-50 dark:ring-offset-gray-900' : ''}`} style={{backgroundColor: c}} />
  34. ))}
  35. </div>
  36. <input type="color" value={color} onChange={e => onChange(e.target.value)} className="w-8 h-8 rounded border-none cursor-pointer bg-transparent" />
  37. </div>
  38. </div>
  39. );
  40. const predefinedGradients = [
  41. { name: 'Abyss', value: 'bg-gradient-to-br from-gray-800 to-gray-900' },
  42. { name: 'Emerald', value: 'bg-gradient-to-br from-green-500 to-teal-600' },
  43. { name: 'Sunset', value: 'bg-gradient-to-br from-red-500 to-yellow-500' },
  44. { name: 'Violet', value: 'bg-gradient-to-br from-purple-600 to-indigo-700' },
  45. ];
  46. const predefinedBackgrounds: MediaSource[] = [
  47. { type: 'url', value: 'https://picsum.photos/seed/bg-beach/1200/800' },
  48. { type: 'url', value: 'https://picsum.photos/seed/bg-music/1200/800' },
  49. { type: 'url', value: 'https://picsum.photos/seed/bg-city/1200/800' },
  50. { type: 'url', value: 'https://picsum.photos/seed/bg-dock/1200/800' },
  51. ];
  52. const getImageUrl = (source: MediaSource): string => {
  53. if (source.type === 'url') return source.value;
  54. if (source.type === 'file') return source.value.previewUrl;
  55. return ''; // AIGC not applicable for backgrounds
  56. }
  57. const themes: { name: string; value: ThemeName }[] = [
  58. { name: 'Light', value: 'light' }, { name: 'Dark', value: 'dark' }, { name: 'Synthwave', value: 'synthwave' }, { name: 'Retro', value: 'retro' },
  59. ];
  60. const fontSizes = [
  61. { name: 'Small', value: 'text-sm' }, { name: 'Base', value: 'text-base' }, { name: 'Large', value: 'text-lg' },
  62. ];
  63. const designTemplates: { name: string, settings: Partial<DesignSettings> & { sideNavSettings: Partial<SideNavSettings>, bannerSettings: Partial<BannerSettings> } }[] = [
  64. { name: 'Midnight Emerald', settings: { theme: 'dark', fontColor: '#A7F3D0', backgroundType: 'gradient', backgroundValue: 'bg-gradient-to-br from-emerald-900 via-gray-900 to-black', buttonStyle: 'outline', buttonShape: 'rounded', fontFamily: 'sans', sideNavSettings: { backgroundColor: '#064e3b', textColor: '#a7f3d0', activeLinkColor: '#10b981', hoverBackgroundColor: '#047857', hoverTextColor: '#ffffff', fontFamily: 'sans', fontSize: '14px' }, bannerSettings: { type: 'image', value: '', imageSource: { type: 'url', value: 'https://picsum.photos/seed/emerald-city/1200/300' }, height: 300, width: 'contained' } } },
  65. { name: 'Aspen Light', settings: { theme: 'light', fontColor: '#374151', backgroundType: 'color', backgroundValue: '#f9fafb', buttonStyle: 'filled', buttonShape: 'pill', fontFamily: 'sans', sideNavSettings: { backgroundColor: '#ffffff', textColor: '#4b5563', activeLinkColor: '#d1d5db', hoverBackgroundColor: '#f3f4f6', hoverTextColor: '#111827', fontFamily: 'sans', fontSize: '14px' }, bannerSettings: { type: 'image', value: '', imageSource: { type: 'url', value: 'https://picsum.photos/seed/aspen/1200/300' }, height: 300, width: 'contained' } } },
  66. { name: 'Sakura Dream', settings: { theme: 'dark', fontColor: '#fbcfe8', backgroundType: 'gradient', backgroundValue: 'bg-gradient-to-br from-gray-900 via-fuchsia-900/50 to-gray-900', buttonStyle: 'outline', buttonShape: 'pill', fontFamily: 'serif', sideNavSettings: { backgroundColor: '#581c87', textColor: '#fbcfe8', activeLinkColor: '#c026d3', hoverBackgroundColor: '#7e22ce', hoverTextColor: '#ffffff', fontFamily: 'serif', fontSize: '14px' }, bannerSettings: { type: 'image', value: '', imageSource: { type: 'url', value: 'https://picsum.photos/seed/sakura/1200/300' }, height: 300, width: 'contained' } } },
  67. { name: 'Golden Sands', settings: { theme: 'light', fontColor: '#78350f', backgroundType: 'gradient', backgroundValue: 'bg-gradient-to-br from-amber-100 to-yellow-200', buttonStyle: 'filled', buttonShape: 'rounded', fontFamily: 'serif', sideNavSettings: { backgroundColor: '#fef3c7', textColor: '#92400e', activeLinkColor: '#f59e0b', hoverBackgroundColor: '#fcd34d', hoverTextColor: '#78350f', fontFamily: 'serif', fontSize: '14px' }, bannerSettings: { type: 'image', value: '', imageSource: { type: 'url', value: 'https://picsum.photos/seed/sands/1200/300' }, height: 300, width: 'contained' } } },
  68. { name: 'Neo Tokyo', settings: { theme: 'dark', fontColor: '#a5b4fc', backgroundType: 'gradient', backgroundValue: 'bg-gradient-to-br from-black via-indigo-900 to-fuchsia-800', buttonStyle: 'outline', buttonShape: 'square', fontFamily: 'mono', sideNavSettings: { backgroundColor: '#1e1b4b', textColor: '#c7d2fe', activeLinkColor: '#4f46e5', hoverBackgroundColor: '#3730a3', hoverTextColor: '#ffffff', fontFamily: 'mono', fontSize: '14px' }, bannerSettings: { type: 'image', value: '', imageSource: { type: 'url', value: 'https://picsum.photos/seed/tokyo-night/1200/300' }, height: 300, width: 'contained' } } },
  69. { name: 'Arctic Dawn', settings: { theme: 'light', fontColor: '#1e3a8a', backgroundType: 'gradient', backgroundValue: 'bg-gradient-to-br from-blue-100 via-white to-blue-50', buttonStyle: 'filled', buttonShape: 'pill', fontFamily: 'sans', sideNavSettings: { backgroundColor: '#eff6ff', textColor: '#1e3a8a', activeLinkColor: '#60a5fa', hoverBackgroundColor: '#dbeafe', hoverTextColor: '#1e293b', fontFamily: 'sans', fontSize: '14px' }, bannerSettings: { type: 'image', value: '', imageSource: { type: 'url', value: 'https://picsum.photos/seed/arctic/1200/300' }, height: 300, width: 'contained' } } },
  70. { name: 'Mahogany', settings: { theme: 'dark', fontColor: '#fed7aa', backgroundType: 'color', backgroundValue: '#292524', buttonStyle: 'filled', buttonShape: 'square', fontFamily: 'serif', sideNavSettings: { backgroundColor: '#44403c', textColor: '#fed7aa', activeLinkColor: '#f97316', hoverBackgroundColor: '#9a3412', hoverTextColor: '#ffffff', fontFamily: 'serif', fontSize: '14px' }, bannerSettings: { type: 'image', value: '', imageSource: { type: 'url', value: 'https://picsum.photos/seed/mahogany/1200/300' }, height: 300, width: 'contained' } } },
  71. { name: 'Oceanic Deep', settings: { theme: 'dark', fontColor: '#67e8f9', backgroundType: 'gradient', backgroundValue: 'bg-gradient-to-tr from-gray-900 to-teal-900', buttonStyle: 'outline', buttonShape: 'rounded', fontFamily: 'sans', sideNavSettings: { backgroundColor: '#134e4a', textColor: '#99f6e4', activeLinkColor: '#0d9488', hoverBackgroundColor: '#115e59', hoverTextColor: '#ccfbf1', fontFamily: 'sans', fontSize: '14px' }, bannerSettings: { type: 'image', value: '', imageSource: { type: 'url', value: 'https://picsum.photos/seed/oceanic/1200/300' }, height: 300, width: 'contained' } } },
  72. { name: 'Scarlet & Slate', settings: { theme: 'dark', fontColor: '#f1f5f9', backgroundType: 'color', backgroundValue: '#1e293b', buttonStyle: 'filled', buttonShape: 'square', fontFamily: 'sans', sideNavSettings: { backgroundColor: '#0f172a', textColor: '#cbd5e1', activeLinkColor: '#dc2626', hoverBackgroundColor: '#475569', hoverTextColor: '#ffffff', fontFamily: 'sans', fontSize: '14px' }, bannerSettings: { type: 'image', value: '', imageSource: { type: 'url', value: 'https://picsum.photos/seed/scarlet-slate/1200/300' }, height: 300, width: 'contained' } } },
  73. { name: 'Minimalist White', settings: { theme: 'light', fontColor: '#334155', backgroundType: 'color', backgroundValue: '#ffffff', buttonStyle: 'outline', buttonShape: 'square', fontFamily: 'sans', sideNavSettings: { backgroundColor: '#f1f5f9', textColor: '#475569', activeLinkColor: '#94a3b8', hoverBackgroundColor: '#e2e8f0', hoverTextColor: '#0f172a', fontFamily: 'sans', fontSize: '14px' }, bannerSettings: { type: 'image', value: '', imageSource: { type: 'url', value: 'https://picsum.photos/seed/minimal-white/1200/300' }, height: 300, width: 'contained' } } },
  74. ];
  75. const extendedDesignTemplates: { name: string, settings: Partial<DesignSettings> & { sideNavSettings: Partial<SideNavSettings>, bannerSettings: Partial<BannerSettings> } }[] = [
  76. { name: 'Cyberpunk Sunset', settings: { theme: 'dark' as ThemeName, fontColor: '#f472b6', backgroundType: 'gradient' as BackgroundType, backgroundValue: 'bg-gradient-to-br from-indigo-900 via-purple-900 to-pink-900', buttonStyle: 'outline' as ButtonStyle, buttonShape: 'square' as ButtonShape, fontFamily: 'mono' as FontFamily, sideNavSettings: { backgroundColor: '#2b1c58', textColor: '#f472b6', activeLinkColor: '#a855f7', hoverBackgroundColor: '#4c1d95', hoverTextColor: '#f5d0fe', fontFamily: 'mono' as FontFamily, fontSize: '14px' }, bannerSettings: { type: 'image' as BannerType, value: '', imageSource: { type: 'url' as const, value: 'https://picsum.photos/seed/cyberpunk/1200/300' }, height: 300, width: 'contained' as 'contained' } } },
  77. { name: 'Rustic Charm', settings: { theme: 'light' as ThemeName, fontColor: '#3f3f46', backgroundType: 'color' as BackgroundType, backgroundValue: '#f5f5f4', buttonStyle: 'filled' as ButtonStyle, buttonShape: 'rounded' as ButtonShape, fontFamily: 'serif' as FontFamily, sideNavSettings: { backgroundColor: '#e7e5e4', textColor: '#57534e', activeLinkColor: '#a8a29e', hoverBackgroundColor: '#d6d3d1', hoverTextColor: '#1c1917', fontFamily: 'serif' as FontFamily, fontSize: '14px' }, bannerSettings: { type: 'image' as BannerType, value: '', imageSource: { type: 'url' as const, value: 'https://picsum.photos/seed/rustic/1200/300' }, height: 300, width: 'contained' as 'contained' } } },
  78. { name: 'Ocean Breeze', settings: { theme: 'light' as ThemeName, fontColor: '#0c4a6e', backgroundType: 'gradient' as BackgroundType, backgroundValue: 'bg-gradient-to-br from-sky-200 to-blue-200', buttonStyle: 'filled' as ButtonStyle, buttonShape: 'pill' as ButtonShape, fontFamily: 'sans' as FontFamily, sideNavSettings: { backgroundColor: '#e0f2fe', textColor: '#075985', activeLinkColor: '#38bdf8', hoverBackgroundColor: '#bae6fd', hoverTextColor: '#0c4a6e', fontFamily: 'sans' as FontFamily, fontSize: '14px' }, bannerSettings: { type: 'image' as BannerType, value: '', imageSource: { type: 'url' as const, value: 'https://picsum.photos/seed/breeze/1200/300' }, height: 300, width: 'contained' as 'contained' } } },
  79. { name: 'Vintage Noir', settings: { theme: 'dark' as ThemeName, fontColor: '#d6d3d1', backgroundType: 'color' as BackgroundType, backgroundValue: '#1c1917', buttonStyle: 'outline' as ButtonStyle, buttonShape: 'square' as ButtonShape, fontFamily: 'serif' as FontFamily, sideNavSettings: { backgroundColor: '#292524', textColor: '#a8a29e', activeLinkColor: '#facc15', hoverBackgroundColor: '#44403c', hoverTextColor: '#fafafa', fontFamily: 'serif' as FontFamily, fontSize: '14px' }, bannerSettings: { type: 'image' as BannerType, value: '', imageSource: { type: 'url' as const, value: 'https://picsum.photos/seed/noir/1200/300' }, height: 300, width: 'contained' as 'contained' } } },
  80. { name: 'Jungle Expedition', settings: { theme: 'light' as ThemeName, fontColor: '#1a2e05', backgroundType: 'gradient' as BackgroundType, backgroundValue: 'bg-gradient-to-br from-lime-100 via-green-100 to-lime-200', buttonStyle: 'filled' as ButtonStyle, buttonShape: 'pill' as ButtonShape, fontFamily: 'sans' as FontFamily, sideNavSettings: { backgroundColor: '#dcfce7', textColor: '#166534', activeLinkColor: '#4ade80', hoverBackgroundColor: '#bbf7d0', hoverTextColor: '#14532d', fontFamily: 'sans' as FontFamily, fontSize: '14px' }, bannerSettings: { type: 'image' as BannerType, value: '', imageSource: { type: 'url' as const, value: 'https://picsum.photos/seed/jungle/1200/300' }, height: 300, width: 'contained' as 'contained' } } },
  81. { name: 'Cosmic Rift', settings: { theme: 'dark' as ThemeName, fontColor: '#e9d5ff', backgroundType: 'gradient' as BackgroundType, backgroundValue: 'bg-[radial-gradient(ellipse_at_bottom,_var(--tw-gradient-stops))] from-gray-700 via-gray-900 to-black', buttonStyle: 'outline' as ButtonStyle, buttonShape: 'pill' as ButtonShape, fontFamily: 'mono' as FontFamily, sideNavSettings: { backgroundColor: '#1e293b', textColor: '#93c5fd', activeLinkColor: '#60a5fa', hoverBackgroundColor: '#334155', hoverTextColor: '#dbeafe', fontFamily: 'mono' as FontFamily, fontSize: '14px' }, bannerSettings: { type: 'image' as BannerType, value: '', imageSource: { type: 'url' as const, value: 'https://picsum.photos/seed/cosmic/1200/300' }, height: 300, width: 'contained' as 'contained' } } },
  82. { name: 'Autumn Glow', settings: { theme: 'light' as ThemeName, fontColor: '#422006', backgroundType: 'color' as BackgroundType, backgroundValue: '#fff7ed', buttonStyle: 'filled' as ButtonStyle, buttonShape: 'rounded' as ButtonShape, fontFamily: 'serif' as FontFamily, sideNavSettings: { backgroundColor: '#fed7aa', textColor: '#9a3412', activeLinkColor: '#fb923c', hoverBackgroundColor: '#fdba74', hoverTextColor: '#7c2d12', fontFamily: 'serif' as FontFamily, fontSize: '14px' }, bannerSettings: { type: 'image' as BannerType, value: '', imageSource: { type: 'url' as const, value: 'https://picsum.photos/seed/autumn/1200/300' }, height: 300, width: 'contained' as 'contained' } } },
  83. { name: 'Concrete Jungle', settings: { theme: 'dark' as ThemeName, fontColor: '#e5e7eb', backgroundType: 'color' as BackgroundType, backgroundValue: '#27272a', buttonStyle: 'outline' as ButtonStyle, buttonShape: 'square' as ButtonShape, fontFamily: 'sans' as FontFamily, sideNavSettings: { backgroundColor: '#3f3f46', textColor: '#d4d4d8', activeLinkColor: '#a1a1aa', hoverBackgroundColor: '#52525b', hoverTextColor: '#fafafa', fontFamily: 'sans' as FontFamily, fontSize: '14px' }, bannerSettings: { type: 'image' as BannerType, value: '', imageSource: { type: 'url' as const, value: 'https://picsum.photos/seed/concrete/1200/300' }, height: 300, width: 'contained' as 'contained' } } },
  84. { name: 'Pastel Playground', settings: { theme: 'light' as ThemeName, fontColor: '#57534e', backgroundType: 'color' as BackgroundType, backgroundValue: '#fdf2f8', buttonStyle: 'filled' as ButtonStyle, buttonShape: 'pill' as ButtonShape, fontFamily: 'sans' as FontFamily, sideNavSettings: { backgroundColor: '#fce7f3', textColor: '#9d174d', activeLinkColor: '#f472b6', hoverBackgroundColor: '#fbcfe8', hoverTextColor: '#831843', fontFamily: 'sans' as FontFamily, fontSize: '14px' }, bannerSettings: { type: 'image' as BannerType, value: '', imageSource: { type: 'url' as const, value: 'https://picsum.photos/seed/pastel/1200/300' }, height: 300, width: 'contained' as 'contained' } } },
  85. { name: 'Deep Space', settings: { theme: 'dark' as ThemeName, fontColor: '#bfdbfe', backgroundType: 'gradient' as BackgroundType, backgroundValue: 'bg-gradient-to-tl from-gray-900 via-slate-900 to-blue-900', buttonStyle: 'outline' as ButtonStyle, buttonShape: 'rounded' as ButtonShape, fontFamily: 'mono' as FontFamily, sideNavSettings: { backgroundColor: '#0f172a', textColor: '#60a5fa', activeLinkColor: '#3b82f6', hoverBackgroundColor: '#1e293b', hoverTextColor: '#93c5fd', fontFamily: 'mono' as FontFamily, fontSize: '14px' }, bannerSettings: { type: 'image' as BannerType, value: '', imageSource: { type: 'url' as const, value: 'https://picsum.photos/seed/space/1200/300' }, height: 300, width: 'contained' as 'contained' } } },
  86. ].concat(Array.from({ length: 20 }).map((_, i) => ({
  87. name: `Pro Theme ${i + 1}`,
  88. settings: {
  89. theme: (['dark', 'light'] as const)[i % 2],
  90. fontColor: i % 2 === 0 ? '#e0e7ff' : '#1e3a8a',
  91. backgroundType: 'gradient' as const,
  92. backgroundValue: "bg-gradient-to-br from-slate-800 to-indigo-900",
  93. buttonStyle: (['filled', 'outline'] as const)[i % 2],
  94. buttonShape: (['rounded', 'pill', 'square'] as const)[i % 3],
  95. fontFamily: (['sans', 'serif', 'mono'] as const)[i % 3],
  96. sideNavSettings: {
  97. backgroundColor: i % 2 === 0 ? '#1e293b' : '#eef2ff',
  98. textColor: i % 2 === 0 ? '#94a3b8' : '#312e81',
  99. activeLinkColor: i % 2 === 0 ? '#4f46e5' : '#818cf8',
  100. hoverBackgroundColor: i % 2 === 0 ? '#334155' : '#c7d2fe',
  101. hoverTextColor: i % 2 === 0 ? '#e0e7ff' : '#1e1b4b',
  102. fontFamily: (['sans', 'serif', 'mono'] as const)[i % 3],
  103. fontSize: '14px',
  104. },
  105. bannerSettings: { type: 'image' as const, value: '', imageSource: { type: 'url' as const, value: `https://picsum.photos/seed/pro-theme-${i + 1}/1200/300` }, height: 300, width: 'contained' as const },
  106. }
  107. })));
  108. interface DesignEditorProps {
  109. design: DesignSettings;
  110. setDesign: (newDesign: DesignSettings) => void;
  111. }
  112. const DesignEditor: React.FC<DesignEditorProps> = ({ design, setDesign }) => {
  113. const { t } = useTranslation();
  114. const [isThemeModalOpen, setIsThemeModalOpen] = React.useState(false);
  115. const updateDesign = (key: keyof DesignSettings, value: any) => {
  116. setDesign({ ...design, [key]: value });
  117. };
  118. const updateNested = (parentKey: keyof DesignSettings, childKey: string, value: any) => {
  119. updateDesign(parentKey, { ...(design[parentKey] as any), [childKey]: value });
  120. };
  121. const applyThemeTemplate = (templateSettings: any) => {
  122. const newSettings = JSON.parse(JSON.stringify(templateSettings));
  123. const fullNewDesign = {
  124. ...design,
  125. ...newSettings,
  126. bannerSettings: {
  127. ...design.bannerSettings,
  128. ...newSettings.bannerSettings,
  129. },
  130. sideNavSettings: {
  131. ...design.sideNavSettings,
  132. ...newSettings.sideNavSettings,
  133. },
  134. };
  135. if (!fullNewDesign.bannerSettings.height) fullNewDesign.bannerSettings.height = 300;
  136. if (!fullNewDesign.bannerSettings.width) fullNewDesign.bannerSettings.width = 'contained';
  137. setDesign(fullNewDesign);
  138. };
  139. const handleUploadImage = () => {
  140. const newImage: MediaSource = {
  141. type: 'file',
  142. value: {
  143. name: `user_upload_${Date.now()}.jpg`,
  144. size: 123456,
  145. previewUrl: `https://picsum.photos/seed/user-bg-${Date.now()}/400/300`
  146. }
  147. };
  148. const updatedImages = [...(design.userBackgroundImages || []), newImage];
  149. setDesign({ ...design, userBackgroundImages: updatedImages });
  150. };
  151. return (
  152. <div className="space-y-1">
  153. <Accordion title={t('design_editor.templates')} defaultOpen={true}>
  154. <div className="grid grid-cols-2 gap-3">
  155. {designTemplates.map(template => (
  156. <button key={template.name} onMouseDown={(e) => e.preventDefault()} onClick={() => applyThemeTemplate(template.settings)} className="p-2 rounded-lg border-2 border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-brand-primary transition-colors text-left">
  157. <div className="h-12 w-full rounded-md mb-2 bg-cover bg-center" style={{backgroundImage: `url(${(template.settings.bannerSettings as any)?.imageSource?.value})`}} />
  158. <p className="font-semibold text-sm">{template.name}</p>
  159. </button>
  160. ))}
  161. <button onClick={() => setIsThemeModalOpen(true)} className="col-span-2 p-4 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600 hover:border-brand-primary hover:bg-gray-100 dark:hover:bg-gray-900/50 transition-colors flex items-center justify-center">
  162. <span className="font-semibold text-gray-500 dark:text-gray-300">{t('design_editor.more_themes')}</span>
  163. </button>
  164. </div>
  165. </Accordion>
  166. <Accordion title={t('design_editor.profile')}>
  167. <div className="space-y-2 p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
  168. <h4 className="font-semibold mb-2">{t('design_editor.avatar')}</h4>
  169. <div className="flex items-center gap-2">
  170. <label className="text-xs text-gray-500 dark:text-gray-400">{t('design_editor.source')}:</label>
  171. <button onMouseDown={(e) => e.preventDefault()} onClick={() => {
  172. const newSource: MediaSource = { type: 'url', value: '' };
  173. updateDesign('avatarSource', newSource);
  174. }} className={`px-3 py-1 text-xs rounded-md ${design.avatarSource?.type === 'url' ? 'bg-brand-primary text-white' : 'bg-gray-200 dark:bg-gray-600'}`}>URL</button>
  175. <button onMouseDown={(e) => e.preventDefault()} onClick={() => {
  176. const newSource: MediaSource = { type: 'file', value: { name: 'avatar.jpg', size: 12345, previewUrl: `https://api.dicebear.com/8.x/adventurer/svg?seed=NewAvatar` } };
  177. updateDesign('avatarSource', newSource);
  178. }} className={`px-3 py-1 text-xs rounded-md ${design.avatarSource?.type === 'file' ? 'bg-brand-primary text-white' : 'bg-gray-200 dark:bg-gray-600'}`}>{t('link_editor.upload')}</button>
  179. </div>
  180. {design.avatarSource?.type === 'url' ?
  181. <input type="url" placeholder="Enter image URL..." value={design.avatarSource.value} onChange={e => {
  182. const newSource: MediaSource = { type: 'url', value: e.target.value };
  183. updateDesign('avatarSource', newSource);
  184. }} className="w-full bg-gray-200 dark:bg-gray-700 p-2 rounded-md border border-gray-300 dark:border-gray-600" />
  185. :
  186. design.avatarSource?.type === 'file' ?
  187. <div className="text-sm p-2 bg-gray-200 dark:bg-gray-700 rounded-md">Simulated Upload: {design.avatarSource.value.name}</div>
  188. : <div className="text-sm p-2 text-gray-500">No avatar set.</div>
  189. }
  190. </div>
  191. </Accordion>
  192. <Accordion title={t('design_editor.appearance')}>
  193. <>
  194. <div className="grid grid-cols-2 gap-4">
  195. {themes.map(th => <button key={th.value} onMouseDown={(e) => e.preventDefault()} onClick={() => updateDesign('theme', th.value)} className={`p-4 rounded-lg border-2 transition-colors ${design.theme === th.value ? 'border-brand-primary bg-gray-200 dark:bg-gray-700' : 'border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-brand-primary'}`}><p className="font-semibold">{th.name}</p></button>)}
  196. <button onMouseDown={(e) => e.preventDefault()} onClick={() => updateDesign('theme', 'custom')} className={`p-4 rounded-lg border-2 transition-colors ${design.theme === 'custom' ? 'border-brand-primary bg-gray-200 dark:bg-gray-700' : 'border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-brand-primary'}`}><p className="font-semibold">{t('design_editor.custom')}</p></button>
  197. </div>
  198. {design.theme === 'custom' && (
  199. <div className="p-4 bg-gray-100 dark:bg-gray-800 rounded-lg space-y-3 mt-4">
  200. <ColorPalettePicker label={t('design_editor.background_color')} color={design.customThemeColors.background} onChange={c => updateNested('customThemeColors', 'background', c)} />
  201. <ColorPalettePicker label={t('design_editor.text_color')} color={design.customThemeColors.text} onChange={c => updateNested('customThemeColors', 'text', c)} />
  202. <ColorPalettePicker label={t('design_editor.button_color')} color={design.customThemeColors.button} onChange={c => updateNested('customThemeColors', 'button', c)} />
  203. <ColorPalettePicker label={t('design_editor.button_text')} color={design.customThemeColors.buttonText} onChange={c => updateNested('customThemeColors', 'buttonText', c)} />
  204. </div>
  205. )}
  206. </>
  207. </Accordion>
  208. <Accordion title={t('design_editor.typography')}>
  209. <div className="space-y-4">
  210. <ColorPalettePicker label={t('design_editor.font_color')} color={design.fontColor} onChange={c => updateDesign('fontColor', c)} />
  211. <div>
  212. <label className="text-sm text-gray-600 dark:text-gray-300">{t('design_editor.font_size')}</label>
  213. <select value={design.fontSize} onChange={e => updateDesign('fontSize', e.target.value)} className="w-full bg-gray-100 dark:bg-gray-700 p-2 rounded-md border border-gray-300 dark:border-gray-600 mt-1">
  214. {fontSizes.map(fs => <option key={fs.value} value={fs.value}>{fs.name}</option>)}
  215. </select>
  216. </div>
  217. <div>
  218. <label className="text-sm text-gray-600 dark:text-gray-300">{t('design_editor.font_family')}</label>
  219. <select value={design.fontFamily} onChange={e => updateDesign('fontFamily', e.target.value as FontFamily)} className="w-full bg-gray-100 dark:bg-gray-700 p-2 rounded-md border border-gray-300 dark:border-gray-600 mt-1">
  220. <option value="sans">Sans-Serif</option><option value="serif">Serif</option><option value="mono">Monospaced</option>
  221. </select>
  222. </div>
  223. </div>
  224. </Accordion>
  225. <Accordion title={t('design_editor.buttons')}>
  226. <div className="space-y-4">
  227. <div>
  228. <h4 className="text-sm font-semibold mb-2">{t('design_editor.button_style')}</h4>
  229. <div className="flex gap-2">
  230. {(['filled', 'outline'] as ButtonStyle[]).map(style => <button key={style} onMouseDown={(e) => e.preventDefault()} onClick={() => updateDesign('buttonStyle', style)} className={`px-4 py-2 text-sm rounded-md capitalize flex-1 border-2 ${design.buttonStyle === style ? 'bg-brand-primary border-brand-primary text-white' : 'bg-transparent border-gray-300 dark:border-gray-600'}`}>{style}</button>)}
  231. </div>
  232. </div>
  233. <div>
  234. <h4 className="text-sm font-semibold mb-2">{t('design_editor.button_shape')}</h4>
  235. <div className="flex gap-2">
  236. {(['rounded', 'pill', 'square'] as ButtonShape[]).map(shape => <button key={shape} onMouseDown={(e) => e.preventDefault()} onClick={() => updateDesign('buttonShape', shape)} className={`px-4 py-2 text-sm flex-1 border-2 ${design.buttonShape === shape ? 'bg-brand-primary border-brand-primary text-white' : 'bg-transparent border-gray-300 dark:border-gray-600'} ${shape === 'rounded' ? 'rounded-md' : shape === 'pill' ? 'rounded-full' : ''}`}>{shape}</button>)}
  237. </div>
  238. </div>
  239. </div>
  240. </Accordion>
  241. <Accordion title={t('design_editor.background')}>
  242. <div>
  243. <div className="flex gap-2 mb-4">
  244. {(['color', 'gradient', 'image'] as BackgroundType[]).map(type =>
  245. <button key={type} onMouseDown={(e) => e.preventDefault()} onClick={() => {
  246. let newValue = design.backgroundValue;
  247. if (type === 'color' && design.backgroundType !== 'color') newValue = '#1f2937';
  248. if (type === 'gradient' && design.backgroundType !== 'gradient') newValue = predefinedGradients[0].value;
  249. if (type === 'image' && design.backgroundType !== 'image') {
  250. const allImages = [...predefinedBackgrounds, ...(design.userBackgroundImages || [])];
  251. newValue = allImages.length > 0 ? getImageUrl(allImages[0]) : '';
  252. };
  253. setDesign({ ...design, backgroundType: type, backgroundValue: newValue });
  254. }}
  255. className={`px-4 py-2 text-sm rounded-md capitalize flex-1 border-2 ${design.backgroundType === type ? 'bg-brand-primary border-brand-primary text-white' : 'bg-transparent border-gray-300 dark:border-gray-600'}`}>{type}</button>
  256. )}
  257. </div>
  258. {design.backgroundType === 'color' && <ColorPalettePicker label={t('design_editor.background_color')} color={design.backgroundValue} onChange={c => updateDesign('backgroundValue', c)} />}
  259. {design.backgroundType === 'gradient' && (<div className="grid grid-cols-2 gap-4">{predefinedGradients.map(bg => <button key={bg.name} onMouseDown={(e) => e.preventDefault()} onClick={() => updateDesign('backgroundValue', bg.value)} className={`h-16 rounded-lg ${bg.value} flex items-center justify-center text-white font-semibold transition-all ${design.backgroundValue === bg.value ? 'ring-2 ring-brand-primary ring-offset-2 ring-offset-gray-50 dark:ring-offset-gray-900' : ''}`}>{bg.name}</button>)}</div>)}
  260. {design.backgroundType === 'image' && (
  261. <div className="space-y-4">
  262. <div className="grid grid-cols-2 gap-3">
  263. {[...predefinedBackgrounds, ...(design.userBackgroundImages || [])].map(bg => {
  264. const url = getImageUrl(bg);
  265. const isSelected = design.backgroundValue === url;
  266. return (
  267. <button key={url} onMouseDown={(e) => e.preventDefault()} onClick={() => updateDesign('backgroundValue', url)} className={`relative group rounded-lg overflow-hidden border-2 transition-all h-24 ${isSelected ? 'border-brand-primary' : 'border-transparent'}`}>
  268. <img src={url} className="w-full h-full object-cover" alt="Background option"/>
  269. {isSelected && <div className="absolute inset-0 bg-black/50 flex items-center justify-center"><Icon className="h-6 w-6 text-white"><path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7"/></Icon></div>}
  270. </button>
  271. )
  272. })}
  273. </div>
  274. <button onClick={handleUploadImage} className="w-full p-3 bg-gray-200 dark:bg-gray-700 rounded-lg text-sm font-semibold hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors">
  275. {t('design_editor.upload_image')}
  276. </button>
  277. </div>
  278. )}
  279. </div>
  280. </Accordion>
  281. <Accordion title={t('design_editor.enterprise_layout')} defaultOpen={false}>
  282. <div className="space-y-6">
  283. <div>
  284. <h4 className="font-semibold mb-3">{t('design_editor.banner')}</h4>
  285. <div className="space-y-4 p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
  286. <div>
  287. <label className="text-sm text-gray-600 dark:text-gray-300 block mb-2">{t('design_editor.banner_type')}</label>
  288. <div className="flex gap-2">
  289. {(['color', 'gradient', 'image', 'none'] as BannerType[]).map(type =>
  290. <button key={type} onMouseDown={(e) => e.preventDefault()} onClick={() => {
  291. let newSettings = { ...design.bannerSettings, type };
  292. if (type === 'color' && design.bannerSettings.type !== 'color') newSettings.value = '#374151';
  293. if (type === 'gradient' && design.bannerSettings.type !== 'gradient') newSettings.value = predefinedGradients[0].value;
  294. if (type === 'image' && design.bannerSettings.type !== 'image') {
  295. if (!newSettings.imageSource) newSettings.imageSource = { type: 'url', value: '' };
  296. }
  297. updateDesign('bannerSettings', newSettings);
  298. }} className={`px-4 py-2 text-sm rounded-md capitalize flex-1 border-2 ${design.bannerSettings.type === type ? 'bg-brand-primary border-brand-primary text-white' : 'bg-transparent border-gray-300 dark:border-gray-600'}`}>{type}</button>
  299. )}
  300. </div>
  301. </div>
  302. {design.bannerSettings.type !== 'none' && (
  303. <>
  304. <div>
  305. <label className="text-sm text-gray-600 dark:text-gray-300 block mb-2">{t('design_editor.banner_width')}</label>
  306. <div className="flex gap-2">
  307. <button onMouseDown={(e) => e.preventDefault()} onClick={() => updateNested('bannerSettings', 'width', 'contained')} className={`px-4 py-2 text-sm rounded-md capitalize flex-1 border-2 ${design.bannerSettings.width === 'contained' ? 'bg-brand-primary border-brand-primary text-white' : 'bg-transparent border-gray-300 dark:border-gray-600'}`}>{t('design_editor.contained')}</button>
  308. <button onMouseDown={(e) => e.preventDefault()} onClick={() => updateNested('bannerSettings', 'width', 'full')} className={`px-4 py-2 text-sm rounded-md capitalize flex-1 border-2 ${design.bannerSettings.width === 'full' ? 'bg-brand-primary border-brand-primary text-white' : 'bg-transparent border-gray-300 dark:border-gray-600'}`}>{t('design_editor.full_width')}</button>
  309. </div>
  310. </div>
  311. <div>
  312. <label htmlFor="bannerHeight" className="text-sm text-gray-600 dark:text-gray-300 block mb-1">{t('design_editor.banner_height')}</label>
  313. <input id="bannerHeight" type="number" value={design.bannerSettings.height} onChange={e => updateNested('bannerSettings', 'height', parseInt(e.target.value, 10) || 0)} className="w-full bg-gray-200 dark:bg-gray-700 p-2 rounded-md border border-gray-300 dark:border-gray-600"/>
  314. </div>
  315. {design.bannerSettings.type === 'color' && <ColorPalettePicker label={t('design_editor.banner_color')} color={design.bannerSettings.value} onChange={c => updateNested('bannerSettings', 'value', c)} />}
  316. {design.bannerSettings.type === 'gradient' && (<div className="grid grid-cols-2 gap-4">{predefinedGradients.map(bg => <button key={bg.name} onMouseDown={(e) => e.preventDefault()} onClick={() => updateNested('bannerSettings', 'value', bg.value)} className={`h-16 rounded-lg ${bg.value} flex items-center justify-center text-white font-semibold transition-all ${design.bannerSettings.value === bg.value ? 'ring-2 ring-brand-primary ring-offset-2 ring-offset-gray-50 dark:ring-offset-gray-900' : ''}`}>{bg.name}</button>)}</div>)}
  317. {design.bannerSettings.type === 'image' && (
  318. <div className="space-y-2">
  319. <div className="flex items-center gap-2">
  320. <label className="text-xs text-gray-500 dark:text-gray-400">{t('design_editor.source')}:</label>
  321. <button onMouseDown={(e) => e.preventDefault()} onClick={() => {
  322. const newSource: MediaSource = { type: 'url', value: '' };
  323. updateNested('bannerSettings', 'imageSource', newSource);
  324. }} className={`px-3 py-1 text-xs rounded-md ${design.bannerSettings.imageSource?.type === 'url' ? 'bg-brand-primary text-white' : 'bg-gray-200 dark:bg-gray-600'}`}>URL</button>
  325. <button onMouseDown={(e) => e.preventDefault()} onClick={() => {
  326. const newSource: MediaSource = { type: 'file', value: { name: 'banner.jpg', size: 12345, previewUrl: `https://picsum.photos/seed/upload-${Date.now()}/1200/300` } };
  327. updateNested('bannerSettings', 'imageSource', newSource);
  328. }} className={`px-3 py-1 text-xs rounded-md ${design.bannerSettings.imageSource?.type === 'file' ? 'bg-brand-primary text-white' : 'bg-gray-200 dark:bg-gray-600'}`}>{t('link_editor.upload')}</button>
  329. </div>
  330. {design.bannerSettings.imageSource?.type === 'url' ?
  331. <input type="url" placeholder="Enter image URL..." value={design.bannerSettings.imageSource.value} onChange={e => {
  332. const newSource: MediaSource = { type: 'url', value: e.target.value };
  333. updateNested('bannerSettings', 'imageSource', newSource);
  334. }} className="w-full bg-gray-200 dark:bg-gray-700 p-2 rounded-md border border-gray-300 dark:border-gray-600" />
  335. :
  336. design.bannerSettings.imageSource?.type === 'file' ?
  337. <div className="text-sm p-2 bg-gray-200 dark:bg-gray-700 rounded-md">Simulated Upload: {design.bannerSettings.imageSource.value.name}</div>
  338. : null
  339. }
  340. </div>
  341. )}
  342. </>
  343. )}
  344. </div>
  345. </div>
  346. <div>
  347. <h4 className="font-semibold mb-3">{t('design_editor.side_nav')}</h4>
  348. <div className="space-y-3 p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
  349. <div className="grid grid-cols-2 gap-4">
  350. <div>
  351. <label className="text-sm text-gray-600 dark:text-gray-300">{t('design_editor.font_family')}</label>
  352. <select value={design.sideNavSettings.fontFamily} onChange={e => updateNested('sideNavSettings', 'fontFamily', e.target.value as FontFamily)} className="w-full bg-gray-200 dark:bg-gray-700 p-2 rounded-md border border-gray-300 dark:border-gray-600 mt-1">
  353. <option value="sans">Sans-Serif</option><option value="serif">Serif</option><option value="mono">Monospaced</option>
  354. </select>
  355. </div>
  356. <div>
  357. <label className="text-sm text-gray-600 dark:text-gray-300">{t('design_editor.font_size')}</label>
  358. <input type="text" placeholder="e.g., 14px" value={design.sideNavSettings.fontSize} onChange={e => updateNested('sideNavSettings', 'fontSize', e.target.value)} className="w-full bg-gray-200 dark:bg-gray-700 p-2 rounded-md border border-gray-300 dark:border-gray-600 mt-1" />
  359. </div>
  360. </div>
  361. <div>
  362. <label className="text-sm text-gray-600 dark:text-gray-300 block mb-2">{t('design_editor.position')}</label>
  363. <div className="flex gap-2">
  364. {(['normal', 'top', 'center'] as const).map(style =>
  365. <button key={style} onMouseDown={(e) => e.preventDefault()} onClick={() => updateNested('sideNavSettings', 'navFloatStyle', style)}
  366. className={`px-4 py-2 text-sm rounded-md capitalize flex-1 border-2 ${(design.sideNavSettings.navFloatStyle || 'normal') === style ? 'bg-brand-primary border-brand-primary text-white' : 'bg-transparent border-gray-300 dark:border-gray-600'}`}>
  367. {style === 'top' ? t('design_editor.float_top') : style === 'center' ? t('design_editor.float_center') : t('design_editor.normal')}
  368. </button>
  369. )}
  370. </div>
  371. </div>
  372. <div>
  373. <label className="text-sm text-gray-600 dark:text-gray-300 block mb-2">{t('design_editor.background_style')}</label>
  374. <div className="flex gap-2">
  375. {(['compact', 'full'] as const).map(style =>
  376. <button key={style} onMouseDown={(e) => e.preventDefault()} onClick={() => updateNested('sideNavSettings', 'navBackgroundStyle', style)}
  377. className={`px-4 py-2 text-sm rounded-md capitalize flex-1 border-2 ${(design.sideNavSettings.navBackgroundStyle || 'compact') === style ? 'bg-brand-primary border-brand-primary text-white' : 'bg-transparent border-gray-300 dark:border-gray-600'}`}>
  378. {style === 'full' ? t('design_editor.full_height') : t('design_editor.compact')}
  379. </button>
  380. )}
  381. </div>
  382. </div>
  383. <ColorPalettePicker label={t('design_editor.background_color')} color={design.sideNavSettings.backgroundColor} onChange={c => updateNested('sideNavSettings', 'backgroundColor', c)} />
  384. <ColorPalettePicker label={t('design_editor.text_color')} color={design.sideNavSettings.textColor} onChange={c => updateNested('sideNavSettings', 'textColor', c)} />
  385. <ColorPalettePicker label={t('design_editor.active_link')} color={design.sideNavSettings.activeLinkColor} onChange={c => updateNested('sideNavSettings', 'activeLinkColor', c)} />
  386. <ColorPalettePicker label={t('design_editor.hover_bg')} color={design.sideNavSettings.hoverBackgroundColor} onChange={c => updateNested('sideNavSettings', 'hoverBackgroundColor', c)} />
  387. <ColorPalettePicker label={t('design_editor.hover_text')} color={design.sideNavSettings.hoverTextColor} onChange={c => updateNested('sideNavSettings', 'hoverTextColor', c)} />
  388. </div>
  389. </div>
  390. <div>
  391. <h4 className="font-semibold mb-3">{t('design_editor.chat_widget')}</h4>
  392. <div className="space-y-3 p-4 bg-gray-100 dark:bg-gray-800 rounded-lg">
  393. <ColorPalettePicker label={t('design_editor.icon_color')} color={design.chatWidgetSettings.iconColor} onChange={c => updateNested('chatWidgetSettings', 'iconColor', c)} />
  394. <ColorPalettePicker label={t('design_editor.header_bg')} color={design.chatWidgetSettings.headerBackgroundColor} onChange={c => updateNested('chatWidgetSettings', 'headerBackgroundColor', c)} />
  395. <ColorPalettePicker label={t('design_editor.header_text')} color={design.chatWidgetSettings.headerTextColor} onChange={c => updateNested('chatWidgetSettings', 'headerTextColor', c)} />
  396. <ColorPalettePicker label={t('design_editor.panel_bg')} color={design.chatWidgetSettings.panelBackgroundColor} onChange={c => updateNested('chatWidgetSettings', 'panelBackgroundColor', c)} />
  397. <ColorPalettePicker label={t('design_editor.user_message')} color={design.chatWidgetSettings.userMessageBackgroundColor} onChange={c => updateNested('chatWidgetSettings', 'userMessageBackgroundColor', c)} />
  398. <ColorPalettePicker label={t('design_editor.user_text')} color={design.chatWidgetSettings.userMessageTextColor} onChange={c => updateNested('chatWidgetSettings', 'userMessageTextColor', c)} />
  399. <ColorPalettePicker label={t('design_editor.ai_message')} color={design.chatWidgetSettings.aiMessageBackgroundColor} onChange={c => updateNested('chatWidgetSettings', 'aiMessageBackgroundColor', c)} />
  400. <ColorPalettePicker label={t('design_editor.ai_text')} color={design.chatWidgetSettings.aiMessageTextColor} onChange={c => updateNested('chatWidgetSettings', 'aiMessageTextColor', c)} />
  401. </div>
  402. </div>
  403. </div>
  404. </Accordion>
  405. {isThemeModalOpen && (
  406. <div className="fixed inset-0 bg-black/70 z-50 flex items-center justify-center backdrop-blur-sm" onClick={() => setIsThemeModalOpen(false)}>
  407. <div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-5xl h-[90vh] p-8 border border-gray-200 dark:border-gray-700 flex flex-col" onClick={e => e.stopPropagation()}>
  408. <div className="flex justify-between items-center mb-6 flex-shrink-0">
  409. <h2 className="text-3xl font-bold text-gray-900 dark:text-white">{t('design_editor.more_themes')}</h2>
  410. <button onClick={() => setIsThemeModalOpen(false)} className="text-gray-400 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white">
  411. <Icon className="h-8 w-8"><path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" /></Icon>
  412. </button>
  413. </div>
  414. <div className="flex-1 overflow-y-auto pr-4 -mr-4">
  415. <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
  416. {extendedDesignTemplates.map(template => (
  417. <button key={template.name} onMouseDown={(e) => e.preventDefault()} onClick={() => { applyThemeTemplate(template.settings); setIsThemeModalOpen(false); }} className="p-2 rounded-lg border-2 border-gray-300 dark:border-gray-700 bg-white dark:bg-gray-800 hover:border-brand-primary transition-colors text-left">
  418. <div className="h-20 w-full rounded-md mb-2 bg-cover bg-center" style={{backgroundImage: `url(${(template.settings.bannerSettings as any)?.imageSource?.value})`}} />
  419. <p className="font-semibold text-sm">{template.name}</p>
  420. </button>
  421. ))}
  422. </div>
  423. </div>
  424. </div>
  425. </div>
  426. )}
  427. </div>
  428. );
  429. };
  430. export default DesignEditor;