Explorar el Código

feat(dashboard): Introduce theme variables plugin for dynamic theming (#3452)

David Höck hace 10 meses
padre
commit
d47177c44e

+ 6 - 78
packages/dashboard/src/app/styles.css

@@ -4,84 +4,12 @@
 
 @custom-variant dark (&:is(.dark *));
 
-:root {
-    --background: hsl(0 0% 100%);
-    --foreground: hsl(0 0% 3.9%);
-    --card: hsl(0 0% 100%);
-    --card-foreground: hsl(0 0% 3.9%);
-    --popover: hsl(0 0% 100%);
-    --popover-foreground: hsl(0 0% 3.9%);
-    --primary: hsl(0 0% 9%);
-    --primary-foreground: hsl(0 0% 98%);
-    --secondary: hsl(0 0% 96.1%);
-    --secondary-foreground: hsl(0 0% 9%);
-    --muted: hsl(0 0% 96.1%);
-    --muted-foreground: hsl(0 0% 45.1%);
-    --accent: hsl(0 0% 96.1%);
-    --accent-foreground: hsl(0 0% 9%);
-    --destructive: hsl(0 84.2% 60.2%);
-    --destructive-foreground: hsl(0 0% 98%);
-    --success: hsl(100, 81%, 35%);
-    --success-foreground: hsl(0 0% 98%);
-    --dev-mode: hsl(204, 76%, 62%);
-    --dev-mode-foreground: hsl(0 0% 98%);
-    --border: hsl(0 0% 89.8%);
-    --input: hsl(0 0% 89.8%);
-    --ring: hsl(0 0% 3.9%);
-    --chart-1: hsl(12 76% 61%);
-    --chart-2: hsl(173 58% 39%);
-    --chart-3: hsl(197 37% 24%);
-    --chart-4: hsl(43 74% 66%);
-    --chart-5: hsl(27 87% 67%);
-    --radius: 0.6rem;
-    --sidebar: hsl(0 0% 98%);
-    --sidebar-foreground: hsl(240 5.3% 26.1%);
-    --sidebar-primary: hsl(240 5.9% 10%);
-    --sidebar-primary-foreground: hsl(0 0% 98%);
-    --sidebar-accent: hsl(0, 0%, 92%);
-    --sidebar-accent-foreground: hsl(240 5.9% 10%);
-    --sidebar-border: hsl(220 13% 91%);
-    --sidebar-ring: hsl(217.2 91.2% 59.8%);
-}
-
-.dark {
-    --background: hsl(0 0% 3.9%);
-    --foreground: hsl(0 0% 98%);
-    --card: hsl(0 0% 3.9%);
-    --card-foreground: hsl(0 0% 98%);
-    --popover: hsl(0 0% 3.9%);
-    --popover-foreground: hsl(0 0% 98%);
-    --primary: hsl(0 0% 98%);
-    --primary-foreground: hsl(0 0% 9%);
-    --secondary: hsl(0 0% 14.9%);
-    --secondary-foreground: hsl(0 0% 98%);
-    --muted: hsl(0 0% 14.9%);
-    --muted-foreground: hsl(0 0% 63.9%);
-    --accent: hsl(0 0% 14.9%);
-    --accent-foreground: hsl(0 0% 98%);
-    --destructive: hsl(0 62.8% 30.6%);
-    --destructive-foreground: hsl(0 0% 98%);
-    --success: hsl(100, 100%, 35%);
-    --success-foreground: hsl(0 0% 98%);
-    --dev-mode: hsl(204, 86%, 53%);
-    --dev-mode-foreground: hsl(0 0% 98%);
-    --border: hsl(0 0% 14.9%);
-    --input: hsl(0 0% 14.9%);
-    --ring: hsl(0 0% 83.1%);
-    --chart-1: hsl(220 70% 50%);
-    --chart-2: hsl(160 60% 45%);
-    --chart-3: hsl(30 80% 55%);
-    --chart-4: hsl(280 65% 60%);
-    --chart-5: hsl(340 75% 55%);
-    --sidebar: hsl(240 5.9% 10%);
-    --sidebar-foreground: hsl(240 4.8% 95.9%);
-    --sidebar-primary: hsl(224.3 76.3% 48%);
-    --sidebar-primary-foreground: hsl(0 0% 100%);
-    --sidebar-accent: hsl(240 3.7% 15.9%);
-    --sidebar-accent-foreground: hsl(240 4.8% 95.9%);
-    --sidebar-border: hsl(240 3.7% 15.9%);
-    --sidebar-ring: hsl(217.2 91.2% 59.8%);
-}
+/*
+* Important: This is not an actual import. We are pre-processing this CSS file
+* to inject the theme variables into the CSS. This import will be replaced
+* with the actual theme variables by the Vite plugin.
+*/
+@import 'virtual:admin-theme';
 
 @theme inline {
     --color-background: var(--background);

+ 217 - 0
packages/dashboard/vite/vite-plugin-theme.ts

@@ -0,0 +1,217 @@
+import { Plugin } from 'vite';
+
+export interface ThemeVariables {
+    light?: {
+        background?: string;
+        foreground?: string;
+        card?: string;
+        'card-foreground'?: string;
+        popover?: string;
+        'popover-foreground'?: string;
+        primary?: string;
+        'primary-foreground'?: string;
+        secondary?: string;
+        'secondary-foreground'?: string;
+        muted?: string;
+        'muted-foreground'?: string;
+        accent?: string;
+        'accent-foreground'?: string;
+        destructive?: string;
+        'destructive-foreground'?: string;
+        success?: string;
+        'success-foreground'?: string;
+        'dev-mode'?: string;
+        'dev-mode-foreground'?: string;
+        border?: string;
+        input?: string;
+        ring?: string;
+        'chart-1'?: string;
+        'chart-2'?: string;
+        'chart-3'?: string;
+        'chart-4'?: string;
+        'chart-5'?: string;
+        radius?: string;
+        sidebar?: string;
+        'sidebar-foreground'?: string;
+        'sidebar-primary'?: string;
+        'sidebar-primary-foreground'?: string;
+        'sidebar-accent'?: string;
+        'sidebar-accent-foreground'?: string;
+        'sidebar-border'?: string;
+        'sidebar-ring'?: string;
+        [key: string]: string | undefined;
+    };
+    dark?: {
+        background?: string;
+        foreground?: string;
+        card?: string;
+        'card-foreground'?: string;
+        popover?: string;
+        'popover-foreground'?: string;
+        primary?: string;
+        'primary-foreground'?: string;
+        secondary?: string;
+        'secondary-foreground'?: string;
+        muted?: string;
+        'muted-foreground'?: string;
+        accent?: string;
+        'accent-foreground'?: string;
+        destructive?: string;
+        'destructive-foreground'?: string;
+        success?: string;
+        'success-foreground'?: string;
+        'dev-mode'?: string;
+        'dev-mode-foreground'?: string;
+        border?: string;
+        input?: string;
+        ring?: string;
+        'chart-1'?: string;
+        'chart-2'?: string;
+        'chart-3'?: string;
+        'chart-4'?: string;
+        'chart-5'?: string;
+        radius?: string;
+        sidebar?: string;
+        'sidebar-foreground'?: string;
+        'sidebar-primary'?: string;
+        'sidebar-primary-foreground'?: string;
+        'sidebar-accent'?: string;
+        'sidebar-accent-foreground'?: string;
+        'sidebar-border'?: string;
+        'sidebar-ring'?: string;
+        [key: string]: string | undefined;
+    };
+}
+
+const defaultVariables: ThemeVariables = {
+    light: {
+        background: 'hsl(0 0% 100%)',
+        foreground: 'hsl(0 0% 3.9%)',
+        card: 'hsl(0 0% 100%)',
+        'card-foreground': 'hsl(0 0% 3.9%)',
+        popover: 'hsl(0 0% 100%)',
+        'popover-foreground': 'hsl(0 0% 3.9%)',
+        primary: 'hsl(0 0% 9%)',
+        'primary-foreground': 'hsl(0 0% 98%)',
+        secondary: 'hsl(0 0% 96.1%)',
+        'secondary-foreground': 'hsl(0 0% 9%)',
+        muted: 'hsl(0 0% 96.1%)',
+        'muted-foreground': 'hsl(0 0% 45.1%)',
+        accent: 'hsl(0 0% 96.1%)',
+        'accent-foreground': 'hsl(0 0% 9%)',
+        destructive: 'hsl(0 84.2% 60.2%)',
+        'destructive-foreground': 'hsl(0 0% 98%)',
+        success: 'hsl(100, 81%, 35%)',
+        'success-foreground': 'hsl(0 0% 98%)',
+        'dev-mode': 'hsl(204, 76%, 62%)',
+        'dev-mode-foreground': 'hsl(0 0% 98%)',
+        border: 'hsl(0 0% 89.8%)',
+        input: 'hsl(0 0% 89.8%)',
+        ring: 'hsl(0 0% 3.9%)',
+        'chart-1': 'hsl(12 76% 61%)',
+        'chart-2': 'hsl(173 58% 39%)',
+        'chart-3': 'hsl(197 37% 24%)',
+        'chart-4': 'hsl(43 74% 66%)',
+        'chart-5': 'hsl(27 87% 67%)',
+        radius: '0.6rem',
+        sidebar: 'hsl(0 0% 98%)',
+        'sidebar-foreground': 'hsl(240 5.3% 26.1%)',
+        'sidebar-primary': 'hsl(240 5.9% 10%)',
+        'sidebar-primary-foreground': 'hsl(0 0% 98%)',
+        'sidebar-accent': 'hsl(0, 0%, 92%)',
+        'sidebar-accent-foreground': 'hsl(240 5.9% 10%)',
+        'sidebar-border': 'hsl(220 13% 91%)',
+        'sidebar-ring': 'hsl(217.2 91.2% 59.8%)',
+    },
+    dark: {
+        background: 'hsl(0 0% 3.9%)',
+        foreground: 'hsl(0 0% 98%)',
+        card: 'hsl(0 0% 3.9%)',
+        'card-foreground': 'hsl(0 0% 98%)',
+        popover: 'hsl(0 0% 3.9%)',
+        'popover-foreground': 'hsl(0 0% 98%)',
+        primary: 'hsl(0 0% 98%)',
+        'primary-foreground': 'hsl(0 0% 9%)',
+        secondary: 'hsl(0 0% 14.9%)',
+        'secondary-foreground': 'hsl(0 0% 98%)',
+        muted: 'hsl(0 0% 14.9%)',
+        'muted-foreground': 'hsl(0 0% 63.9%)',
+        accent: 'hsl(0 0% 14.9%)',
+        'accent-foreground': 'hsl(0 0% 98%)',
+        destructive: 'hsl(0 62.8% 30.6%)',
+        'destructive-foreground': 'hsl(0 0% 98%)',
+        success: 'hsl(100, 100%, 35%)',
+        'success-foreground': 'hsl(0 0% 98%)',
+        'dev-mode': 'hsl(204, 86%, 53%)',
+        'dev-mode-foreground': 'hsl(0 0% 98%)',
+        border: 'hsl(0 0% 14.9%)',
+        input: 'hsl(0 0% 14.9%)',
+        ring: 'hsl(0 0% 83.1%)',
+        'chart-1': 'hsl(220 70% 50%)',
+        'chart-2': 'hsl(160 60% 45%)',
+        'chart-3': 'hsl(30 80% 55%)',
+        'chart-4': 'hsl(280 65% 60%)',
+        'chart-5': 'hsl(340 75% 55%)',
+        sidebar: 'hsl(240 5.9% 10%)',
+        'sidebar-foreground': 'hsl(240 4.8% 95.9%)',
+        'sidebar-primary': 'hsl(224.3 76.3% 48%)',
+        'sidebar-primary-foreground': 'hsl(0 0% 100%)',
+        'sidebar-accent': 'hsl(240 3.7% 15.9%)',
+        'sidebar-accent-foreground': 'hsl(240 4.8% 95.9%)',
+        'sidebar-border': 'hsl(240 3.7% 15.9%)',
+        'sidebar-ring': 'hsl(217.2 91.2% 59.8%)',
+    },
+};
+
+export type ThemeVariablesPluginOptions = {
+    theme?: ThemeVariables;
+};
+
+export function themeVariablesPlugin(options: ThemeVariablesPluginOptions): Plugin {
+    const virtualModuleId = 'virtual:admin-theme';
+    const resolvedVirtualModuleId = `\0${virtualModuleId}`;
+
+    return {
+        name: 'vendure:admin-theme',
+        enforce: 'pre', // This ensures our plugin runs before other CSS processors
+        transform(code, id) {
+            // Only transform CSS files
+            if (!id.endsWith('styles.css')) {
+                return null;
+            }
+
+            // Replace the @import 'virtual:admin-theme'; with our theme variables
+            if (
+                code.includes('@import "virtual:admin-theme";') ||
+                code.includes("@import 'virtual:admin-theme';")
+            ) {
+                const lightTheme = options.theme?.light || {};
+                const darkTheme = options.theme?.dark || {};
+
+                // Merge default themes with custom themes
+                const mergedLightTheme = { ...defaultVariables.light, ...lightTheme };
+                const mergedDarkTheme = { ...defaultVariables.dark, ...darkTheme };
+
+                const themeCSS = `
+                    :root {
+                        ${Object.entries(mergedLightTheme)
+                            .filter(([key, value]) => value !== undefined)
+                            .map(([key, value]) => `--${key}: ${value as string};`)
+                            .join('\n')}
+                    }
+
+                    .dark {
+                        ${Object.entries(mergedDarkTheme)
+                            .filter(([key, value]) => value !== undefined)
+                            .map(([key, value]) => `--${key}: ${value as string};`)
+                            .join('\n')}
+                    }
+                `;
+
+                return code.replace(/@import ['"]virtual:admin-theme['"];?/, themeCSS);
+            }
+
+            return null;
+        },
+    };
+}

+ 5 - 1
packages/dashboard/vite/vite-plugin-vendure-dashboard.ts

@@ -10,6 +10,8 @@ import { configLoaderPlugin } from './vite-plugin-config-loader.js';
 import { viteConfigPlugin } from './vite-plugin-config.js';
 import { dashboardMetadataPlugin } from './vite-plugin-dashboard-metadata.js';
 import { gqlTadaPlugin } from './vite-plugin-gql-tada.js';
+import { themeVariablesPlugin } from './vite-plugin-theme.js';
+import { ThemeVariablesPluginOptions } from './vite-plugin-theme.js';
 import { UiConfigPluginOptions, uiConfigPlugin } from './vite-plugin-ui-config.js';
 
 /**
@@ -35,7 +37,8 @@ export type VitePluginVendureDashboardOptions = {
     gqlTadaOutputPath?: string;
     tempCompilationDir?: string;
     disableTansStackRouterPlugin?: boolean;
-} & UiConfigPluginOptions;
+} & UiConfigPluginOptions &
+    ThemeVariablesPluginOptions;
 
 /**
  * @description
@@ -69,6 +72,7 @@ export function vendureDashboardPlugin(options: VitePluginVendureDashboardOption
             //     plugins: ['@lingui/babel-plugin-lingui-macro'],
             // },
         }),
+        themeVariablesPlugin({ theme: options.theme }),
         tailwindcss(),
         configLoaderPlugin({ vendureConfigPath: normalizedVendureConfigPath, tempDir }),
         viteConfigPlugin({ packageRoot }),