vite-plugin-theme.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import { Plugin } from 'vite';
  2. type ThemeColors = {
  3. background?: string;
  4. foreground?: string;
  5. card?: string;
  6. 'card-foreground'?: string;
  7. popover?: string;
  8. 'popover-foreground'?: string;
  9. primary?: string;
  10. 'primary-foreground'?: string;
  11. secondary?: string;
  12. 'secondary-foreground'?: string;
  13. muted?: string;
  14. 'muted-foreground'?: string;
  15. accent?: string;
  16. 'accent-foreground'?: string;
  17. destructive?: string;
  18. 'destructive-foreground'?: string;
  19. success?: string;
  20. 'success-foreground'?: string;
  21. 'dev-mode'?: string;
  22. 'dev-mode-foreground'?: string;
  23. border?: string;
  24. input?: string;
  25. ring?: string;
  26. 'chart-1'?: string;
  27. 'chart-2'?: string;
  28. 'chart-3'?: string;
  29. 'chart-4'?: string;
  30. 'chart-5'?: string;
  31. radius?: string;
  32. sidebar?: string;
  33. 'sidebar-foreground'?: string;
  34. 'sidebar-primary'?: string;
  35. 'sidebar-primary-foreground'?: string;
  36. 'sidebar-accent'?: string;
  37. 'sidebar-accent-foreground'?: string;
  38. 'sidebar-border'?: string;
  39. 'sidebar-ring'?: string;
  40. brand?: string;
  41. 'brand-lighter'?: string;
  42. 'brand-darker'?: string;
  43. 'font-sans'?: string;
  44. 'font-mono'?: string;
  45. [key: string]: string | undefined;
  46. };
  47. export interface ThemeVariables {
  48. light?: ThemeColors;
  49. dark?: ThemeColors;
  50. }
  51. const defaultVariables: ThemeVariables = {
  52. light: {
  53. background: 'oklch(1.0000 0 0)',
  54. foreground: 'oklch(0.2103 0.0059 285.8852)',
  55. card: 'oklch(1.0000 0 0)',
  56. 'card-foreground': 'oklch(0.2103 0.0059 285.8852)',
  57. popover: 'oklch(1.0000 0 0)',
  58. 'popover-foreground': 'oklch(0.2103 0.0059 285.8852)',
  59. primary: 'oklch(0.7613 0.1503 231.1314)',
  60. 'primary-foreground': 'oklch(0.261 0.043 218.379)',
  61. secondary: 'oklch(0.9674 0.0013 286.3752)',
  62. 'secondary-foreground': 'oklch(0.2103 0.0059 285.8852)',
  63. muted: 'oklch(0.9674 0.0013 286.3752)',
  64. 'muted-foreground': 'oklch(0.5517 0.0138 285.9385)',
  65. accent: 'oklch(0.9674 0.0013 286.3752)',
  66. 'accent-foreground': 'oklch(0.2103 0.0059 285.8852)',
  67. // L=0.60 ensures WCAG AA contrast ratio (4.5:1) against white backgrounds
  68. destructive: 'oklch(0.60 0.24 27.325)',
  69. 'destructive-foreground': 'oklch(0.9851 0 0)',
  70. success: 'hsl(99deg 67.25% 33.2%)',
  71. 'success-foreground': 'hsl(0 0% 98%)',
  72. 'dev-mode': 'hsl(204, 76%, 62%)',
  73. 'dev-mode-foreground': 'hsl(0 0% 98%)',
  74. border: 'oklch(0.9197 0.0040 286.3202)',
  75. input: 'oklch(0.9197 0.0040 286.3202)',
  76. ring: 'oklch(0.7613 0.1503 231.1314)',
  77. 'chart-1': 'oklch(0.7613 0.1503 231.1314)',
  78. 'chart-2': 'oklch(0.5575 0.2525 302.3212)',
  79. 'chart-3': 'oklch(0.5858 0.2220 17.5846)',
  80. 'chart-4': 'oklch(0.6658 0.1574 58.3183)',
  81. 'chart-5': 'oklch(0.6271 0.1699 149.2138)',
  82. radius: '0.375rem',
  83. sidebar: 'oklch(0.9674 0.0013 286.3752)',
  84. 'sidebar-foreground': 'oklch(0.2103 0.0059 285.8852)',
  85. 'sidebar-primary': 'oklch(0.7613 0.1503 231.1314)',
  86. 'sidebar-primary-foreground': 'oklch(0.1408 0.0044 285.8229)',
  87. 'sidebar-accent': 'oklch(1.0000 0 0)',
  88. 'sidebar-accent-foreground': 'oklch(0.2103 0.0059 285.8852)',
  89. 'sidebar-border': 'oklch(0.9197 0.0040 286.3202)',
  90. 'sidebar-ring': 'oklch(0.7613 0.1503 231.1314)',
  91. brand: '#17c1ff',
  92. 'brand-lighter': '#e6f9ff',
  93. 'brand-darker': '#0099ff',
  94. 'font-sans': 'Inter, sans-serif',
  95. 'font-mono': 'Geist Mono, monospace',
  96. },
  97. dark: {
  98. background: 'oklch(0.1408 0.0044 285.8229)',
  99. foreground: 'oklch(0.9851 0 0)',
  100. card: 'oklch(0.2103 0.0059 285.8852)',
  101. 'card-foreground': 'oklch(0.9851 0 0)',
  102. popover: 'oklch(0.2103 0.0059 285.8852)',
  103. 'popover-foreground': 'oklch(0.9851 0 0)',
  104. primary: 'oklch(0.7613 0.1503 231.1314)',
  105. 'primary-foreground': 'oklch(0.1408 0.0044 285.8229)',
  106. secondary: 'oklch(0.2739 0.0055 286.0326)',
  107. 'secondary-foreground': 'oklch(0.9851 0 0)',
  108. muted: 'oklch(0.2739 0.0055 286.0326)',
  109. 'muted-foreground': 'oklch(0.7118 0.0129 286.0665)',
  110. accent: 'oklch(0.2739 0.0055 286.0326)',
  111. 'accent-foreground': 'oklch(0.9851 0 0)',
  112. // L=0.75 ensures WCAG AA contrast ratio (4.5:1) against dark backgrounds
  113. destructive: 'oklch(0.75 0.22 25)',
  114. 'destructive-foreground': 'oklch(0.9851 0 0)',
  115. success: 'hsl(100 76.42% 22.21%)',
  116. 'success-foreground': 'hsl(0 0% 98%)',
  117. 'dev-mode': 'hsl(204, 86%, 53%)',
  118. 'dev-mode-foreground': 'hsl(0 0% 98%)',
  119. border: 'oklch(0.2739 0.0055 286.0326)',
  120. input: 'oklch(0.2739 0.0055 286.0326)',
  121. ring: 'oklch(0.7613 0.1503 231.1314)',
  122. 'chart-1': 'oklch(0.7613 0.1503 231.1314)',
  123. 'chart-2': 'oklch(0.6268 0.2325 303.9004)',
  124. 'chart-3': 'oklch(0.6450 0.2154 16.4393)',
  125. 'chart-4': 'oklch(0.7686 0.1647 70.0804)',
  126. 'chart-5': 'oklch(0.7227 0.1920 149.5793)',
  127. sidebar: 'oklch(0.2 0 0)',
  128. 'sidebar-foreground': 'oklch(0.9851 0 0)',
  129. 'sidebar-primary': 'oklch(0.7613 0.1503 231.1314)',
  130. 'sidebar-primary-foreground': 'oklch(0.1408 0.0044 285.8229)',
  131. 'sidebar-accent': 'oklch(0.2739 0.0055 286.0326)',
  132. 'sidebar-accent-foreground': 'oklch(0.9851 0 0)',
  133. 'sidebar-border': 'oklch(0.2739 0.0055 286.0326)',
  134. 'sidebar-ring': 'oklch(0.7613 0.1503 231.1314)',
  135. brand: '#17c1ff',
  136. 'brand-lighter': '#e6f9ff',
  137. 'brand-darker': '#0099ff',
  138. 'font-sans': 'Inter, sans-serif',
  139. 'font-mono': 'Geist Mono, monospace',
  140. },
  141. };
  142. export type ThemeVariablesPluginOptions = {
  143. theme?: ThemeVariables;
  144. };
  145. /**
  146. * Converts a theme colors object into CSS custom property declarations.
  147. */
  148. function generateCssVariables(theme: ThemeColors): string {
  149. return Object.entries(theme)
  150. .filter(([_, value]) => value !== undefined)
  151. .map(([key, value]) => `--${key}: ${value as string};`)
  152. .join('\n');
  153. }
  154. export function themeVariablesPlugin(options: ThemeVariablesPluginOptions): Plugin {
  155. const virtualModuleId = 'virtual:admin-theme';
  156. const resolvedVirtualModuleId = `\0${virtualModuleId}`;
  157. return {
  158. name: 'vendure:admin-theme',
  159. enforce: 'pre', // This ensures our plugin runs before other CSS processors
  160. transform(code, id) {
  161. // Only transform CSS files
  162. if (!id.endsWith('styles.css')) {
  163. return null;
  164. }
  165. // Replace the @import 'virtual:admin-theme'; with our theme variables
  166. if (
  167. code.includes('@import "virtual:admin-theme";') ||
  168. code.includes("@import 'virtual:admin-theme';")
  169. ) {
  170. const lightTheme = options.theme?.light || {};
  171. const darkTheme = options.theme?.dark || {};
  172. // Merge default themes with custom themes
  173. const mergedLightTheme = { ...defaultVariables.light, ...lightTheme };
  174. const mergedDarkTheme = { ...defaultVariables.dark, ...darkTheme };
  175. const themeCSS = `
  176. :root {
  177. ${generateCssVariables(mergedLightTheme)}
  178. }
  179. .dark {
  180. ${generateCssVariables(mergedDarkTheme)}
  181. }
  182. `;
  183. return code.replace(/@import ['"]virtual:admin-theme['"];?/, themeCSS);
  184. }
  185. return null;
  186. },
  187. };
  188. }