vite-plugin-theme.ts 8.0 KB

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