themes.js 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import fs from 'fs-extra';
  2. import path from 'path';
  3. import { fileURLToPath } from 'url';
  4. const __filename = fileURLToPath(import.meta.url);
  5. const __dirname = path.dirname(__filename);
  6. const THEMES_FILE = path.join(__dirname, 'themes.json');
  7. // Default theme configuration
  8. const DEFAULT_THEME = {
  9. name: 'Default',
  10. id: 'default',
  11. colors: {
  12. primary: '#3b82f6', // blue-500
  13. primaryHover: '#2563eb', // blue-600
  14. secondary: '#6b7280', // gray-500
  15. background: '#ffffff', // white
  16. surface: '#f9fafb', // gray-50
  17. text: '#1f2937', // gray-800
  18. textSecondary: '#6b7280', // gray-500
  19. border: '#e5e7eb', // gray-200
  20. accent: '#10b981', // emerald-500
  21. error: '#ef4444', // red-500
  22. warning: '#f59e0b', // amber-500
  23. success: '#10b981' // emerald-500
  24. },
  25. typography: {
  26. fontFamily: 'Inter, system-ui, sans-serif',
  27. headingFontFamily: 'Inter, system-ui, sans-serif',
  28. fontSize: {
  29. xs: '0.75rem',
  30. sm: '0.875rem',
  31. base: '1rem',
  32. lg: '1.125rem',
  33. xl: '1.25rem',
  34. '2xl': '1.5rem',
  35. '3xl': '1.875rem',
  36. '4xl': '2.25rem'
  37. },
  38. lineHeight: {
  39. tight: '1.25',
  40. normal: '1.5',
  41. relaxed: '1.625',
  42. loose: '2'
  43. }
  44. },
  45. layout: {
  46. borderRadius: {
  47. sm: '0.125rem',
  48. base: '0.25rem',
  49. md: '0.375rem',
  50. lg: '0.5rem',
  51. xl: '0.75rem',
  52. '2xl': '1rem'
  53. },
  54. spacing: {
  55. xs: '0.5rem',
  56. sm: '0.75rem',
  57. base: '1rem',
  58. lg: '1.5rem',
  59. xl: '2rem',
  60. '2xl': '3rem'
  61. },
  62. shadows: {
  63. sm: '0 1px 2px 0 rgb(0 0 0 / 0.05)',
  64. base: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)',
  65. md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
  66. lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)'
  67. }
  68. },
  69. customCSS: '',
  70. createdAt: new Date().toISOString(),
  71. isBuiltIn: true
  72. };
  73. // Built-in theme variations
  74. const BUILT_IN_THEMES = [
  75. DEFAULT_THEME,
  76. {
  77. ...DEFAULT_THEME,
  78. name: 'Dark Mode',
  79. id: 'dark',
  80. colors: {
  81. ...DEFAULT_THEME.colors,
  82. background: '#111827', // gray-900
  83. surface: '#1f2937', // gray-800
  84. text: '#f9fafb', // gray-50
  85. textSecondary: '#9ca3af', // gray-400
  86. border: '#374151' // gray-700
  87. }
  88. },
  89. {
  90. ...DEFAULT_THEME,
  91. name: 'Ocean Blue',
  92. id: 'ocean',
  93. colors: {
  94. ...DEFAULT_THEME.colors,
  95. primary: '#0ea5e9', // sky-500
  96. primaryHover: '#0284c7', // sky-600
  97. accent: '#06b6d4', // cyan-500
  98. surface: '#f0f9ff' // sky-50
  99. }
  100. },
  101. {
  102. ...DEFAULT_THEME,
  103. name: 'Forest Green',
  104. id: 'forest',
  105. colors: {
  106. ...DEFAULT_THEME.colors,
  107. primary: '#059669', // emerald-600
  108. primaryHover: '#047857', // emerald-700
  109. accent: '#10b981', // emerald-500
  110. surface: '#ecfdf5' // emerald-50
  111. }
  112. },
  113. {
  114. ...DEFAULT_THEME,
  115. name: 'Minimalist',
  116. id: 'minimal',
  117. colors: {
  118. ...DEFAULT_THEME.colors,
  119. primary: '#000000',
  120. primaryHover: '#374151',
  121. secondary: '#6b7280',
  122. accent: '#000000'
  123. },
  124. layout: {
  125. ...DEFAULT_THEME.layout,
  126. borderRadius: {
  127. sm: '0',
  128. base: '0',
  129. md: '0',
  130. lg: '0',
  131. xl: '0',
  132. '2xl': '0'
  133. }
  134. }
  135. }
  136. ];
  137. // Initialize themes file
  138. async function initializeThemes() {
  139. try {
  140. if (!(await fs.pathExists(THEMES_FILE))) {
  141. const initialData = {
  142. activeTheme: 'default',
  143. customThemes: [],
  144. builtInThemes: BUILT_IN_THEMES
  145. };
  146. await fs.writeJSON(THEMES_FILE, initialData, { spaces: 2 });
  147. console.log('🎨 Initialized theme system with built-in themes');
  148. }
  149. } catch (error) {
  150. console.error('Error initializing themes:', error);
  151. }
  152. }
  153. // Load themes data
  154. async function loadThemes() {
  155. try {
  156. if (await fs.pathExists(THEMES_FILE)) {
  157. return await fs.readJSON(THEMES_FILE);
  158. }
  159. return { activeTheme: 'default', customThemes: [], builtInThemes: BUILT_IN_THEMES };
  160. } catch (error) {
  161. console.error('Error loading themes:', error);
  162. return { activeTheme: 'default', customThemes: [], builtInThemes: BUILT_IN_THEMES };
  163. }
  164. }
  165. // Save themes data
  166. async function saveThemes(themesData) {
  167. try {
  168. await fs.writeJSON(THEMES_FILE, themesData, { spaces: 2 });
  169. } catch (error) {
  170. console.error('Error saving themes:', error);
  171. throw error;
  172. }
  173. }
  174. // Get all themes (built-in + custom)
  175. export async function getAllThemes() {
  176. const data = await loadThemes();
  177. return {
  178. activeTheme: data.activeTheme,
  179. themes: [...data.builtInThemes, ...data.customThemes]
  180. };
  181. }
  182. // Get active theme
  183. export async function getActiveTheme() {
  184. const data = await loadThemes();
  185. const allThemes = [...data.builtInThemes, ...data.customThemes];
  186. return allThemes.find(theme => theme.id === data.activeTheme) || DEFAULT_THEME;
  187. }
  188. // Set active theme
  189. export async function setActiveTheme(themeId) {
  190. const data = await loadThemes();
  191. const allThemes = [...data.builtInThemes, ...data.customThemes];
  192. if (!allThemes.find(theme => theme.id === themeId)) {
  193. throw new Error('Theme not found');
  194. }
  195. data.activeTheme = themeId;
  196. await saveThemes(data);
  197. return allThemes.find(theme => theme.id === themeId);
  198. }
  199. // Create custom theme
  200. export async function createCustomTheme(themeData) {
  201. const data = await loadThemes();
  202. // Validate required fields
  203. if (!themeData.name || !themeData.id) {
  204. throw new Error('Theme name and id are required');
  205. }
  206. // Check if theme ID already exists
  207. const allThemes = [...data.builtInThemes, ...data.customThemes];
  208. if (allThemes.find(theme => theme.id === themeData.id)) {
  209. throw new Error('Theme ID already exists');
  210. }
  211. const newTheme = {
  212. ...DEFAULT_THEME,
  213. ...themeData,
  214. createdAt: new Date().toISOString(),
  215. isBuiltIn: false
  216. };
  217. data.customThemes.push(newTheme);
  218. await saveThemes(data);
  219. return newTheme;
  220. }
  221. // Update custom theme
  222. export async function updateCustomTheme(themeId, themeData) {
  223. const data = await loadThemes();
  224. const themeIndex = data.customThemes.findIndex(theme => theme.id === themeId);
  225. if (themeIndex === -1) {
  226. throw new Error('Custom theme not found');
  227. }
  228. data.customThemes[themeIndex] = {
  229. ...data.customThemes[themeIndex],
  230. ...themeData,
  231. id: themeId, // Prevent ID changes
  232. updatedAt: new Date().toISOString()
  233. };
  234. await saveThemes(data);
  235. return data.customThemes[themeIndex];
  236. }
  237. // Delete custom theme
  238. export async function deleteCustomTheme(themeId) {
  239. const data = await loadThemes();
  240. const themeIndex = data.customThemes.findIndex(theme => theme.id === themeId);
  241. if (themeIndex === -1) {
  242. throw new Error('Custom theme not found');
  243. }
  244. // If this was the active theme, switch to default
  245. if (data.activeTheme === themeId) {
  246. data.activeTheme = 'default';
  247. }
  248. data.customThemes.splice(themeIndex, 1);
  249. await saveThemes(data);
  250. }
  251. // Export theme configuration
  252. export async function exportTheme(themeId) {
  253. const data = await loadThemes();
  254. const allThemes = [...data.builtInThemes, ...data.customThemes];
  255. const theme = allThemes.find(theme => theme.id === themeId);
  256. if (!theme) {
  257. throw new Error('Theme not found');
  258. }
  259. // Remove internal fields for export
  260. const { createdAt, updatedAt, isBuiltIn, ...exportData } = theme;
  261. return exportData;
  262. }
  263. // Initialize themes on module load
  264. await initializeThemes();