Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
import { ref, watch } from "vue";
import { useSessionStore } from "@/stores/session";
import { usePreferencesStore } from "@/stores/preferences";

export interface ColorSet {
  primary: string;
  secondary: string;
  tertiary: string;
  success: string;
  warning: string;
  danger: string;
  light: string;
  medium: string;
  dark: string;
}

export interface ThemeColors {
  light: ColorSet;
  dark: ColorSet;
}

export interface MenuThemeColors {
  platinum: string;
  gold: string;
  silver: string;
  bronze: string;
  bandaid: string;
}

export const DEFAULT_THEME: ThemeColors = {
  light: {
    primary: "#000000",
    secondary: "#00beff",
    tertiary: "#76dd54",
    success: "#76dd54",
    warning: "#ffea00",
    danger: "#ff1f1f",
    light: "#e3e4e6",
    medium: "#4f5863",
    dark: "#0f172a",
  },
  dark: {
    primary: "#76DD54",
    secondary: "#00beff",
    tertiary: "#F0EA1C",
    success: "#76dd54",
    warning: "#f0ea1c",
    danger: "#dc2626",
    light: "#606666",
    medium: "#454545",
    dark: "#0f172a",
  },
};

export const DEFAULT_MENU_THEME: MenuThemeColors = {
  platinum: "#3db4d6",
  gold: "#ffd83b",
  silver: "#bfbfbf",
  bronze: "#ffad2b",
  bandaid: "#ff8073",
};

export const THEME_PRESETS: Record<string, ThemeColors> = {
  default: DEFAULT_THEME,
  ionic: {
    light: {
      primary: "#0054e9",
      secondary: "#0163aa",
      tertiary: "#6030ff",
      success: "#2dd55b",
      warning: "#ffc409",
      danger: "#c5000f",
      light: "#f4f5f8",
      medium: "#636469",
      dark: "#222428",
    },
    dark: {
      primary: "#4d8dff",
      secondary: "#46b1ff",
      tertiary: "#8482fb",
      success: "#2dd55b",
      warning: "#ffc409",
      danger: "#f24c58",
      light: "#222428",
      medium: "#989aa2",
      dark: "#f4f5f8",
    },
  },
  ocean: {
    light: {
      primary: "#0ea5e9",
      secondary: "#06b6d4",
      tertiary: "#8b5cf6",
      success: "#10b981",
      warning: "#f59e0b",
      danger: "#ef4444",
      light: "#f0f9ff",
      medium: "#64748b",
      dark: "#0c4a6e",
    },
    dark: {
      primary: "#22d3ee",
      secondary: "#14b8a6",
      tertiary: "#a78bfa",
      success: "#34d399",
      warning: "#fbbf24",
      danger: "#f87171",
      light: "#0c4a6e",
      medium: "#94a3b8",
      dark: "#f0f9ff",
    },
  },
  sunset: {
    light: {
      primary: "#f97316",
      secondary: "#ec4899",
      tertiary: "#eab308",
      success: "#22c55e",
      warning: "#f59e0b",
      danger: "#dc2626",
      light: "#fff7ed",
      medium: "#78716c",
      dark: "#7c2d12",
    },
    dark: {
      primary: "#fb923c",
      secondary: "#f472b6",
      tertiary: "#facc15",
      success: "#4ade80",
      warning: "#fbbf24",
      danger: "#ef4444",
      light: "#7c2d12",
      medium: "#a8a29e",
      dark: "#fff7ed",
    },
  },
  forest: {
    light: {
      primary: "#22c55e",
      secondary: "#84cc16",
      tertiary: "#eab308",
      success: "#10b981",
      warning: "#f59e0b",
      danger: "#ef4444",
      light: "#f0fdf4",
      medium: "#6b7280",
      dark: "#14532d",
    },
    dark: {
      primary: "#4ade80",
      secondary: "#a3e635",
      tertiary: "#facc15",
      success: "#34d399",
      warning: "#fbbf24",
      danger: "#f87171",
      light: "#14532d",
      medium: "#9ca3af",
      dark: "#f0fdf4",
    },
  },
  patriot: {
    light: {
      primary: "#1e3a8a",
      secondary: "#dc2626",
      tertiary: "#fbbf24",
      success: "#16a34a",
      warning: "#f59e0b",
      danger: "#b91c1c",
      light: "#f9fafb",
      medium: "#6b7280",
      dark: "#1e293b",
    },
    dark: {
      primary: "#3b82f6",
      secondary: "#ef4444",
      tertiary: "#fde047",
      success: "#22c55e",
      warning: "#fbbf24",
      danger: "#dc2626",
      light: "#1e293b",
      medium: "#9ca3af",
      dark: "#f9fafb",
    },
  },
  steel: {
    light: {
      primary: "#475569",
      secondary: "#64748b",
      tertiary: "#94a3b8",
      success: "#059669",
      warning: "#f59e0b",
      danger: "#dc2626",
      light: "#f1f5f9",
      medium: "#64748b",
      dark: "#334155",
    },
    dark: {
      primary: "#64748b",
      secondary: "#94a3b8",
      tertiary: "#cbd5e1",
      success: "#10b981",
      warning: "#fbbf24",
      danger: "#ef4444",
      light: "#334155",
      medium: "#94a3b8",
      dark: "#f1f5f9",
    },
  },
  corporate: {
    light: {
      primary: "#1e40af",
      secondary: "#6b7280",
      tertiary: "#d4af37",
      success: "#047857",
      warning: "#f59e0b",
      danger: "#991b1b",
      light: "#f3f4f6",
      medium: "#6b7280",
      dark: "#1e3a8a",
    },
    dark: {
      primary: "#3b82f6",
      secondary: "#9ca3af",
      tertiary: "#fbbf24",
      success: "#10b981",
      warning: "#fbbf24",
      danger: "#dc2626",
      light: "#1e3a8a",
      medium: "#9ca3af",
      dark: "#f3f4f6",
    },
  },
  tradesman: {
    light: {
      primary: "#92400e",
      secondary: "#ea580c",
      tertiary: "#fbbf24",
      success: "#15803d",
      warning: "#f59e0b",
      danger: "#dc2626",
      light: "#fef3c7",
      medium: "#78716c",
      dark: "#78350f",
    },
    dark: {
      primary: "#c2410c",
      secondary: "#fb923c",
      tertiary: "#fde047",
      success: "#22c55e",
      warning: "#fbbf24",
      danger: "#ef4444",
      light: "#78350f",
      medium: "#a8a29e",
      dark: "#fef3c7",
    },
  },
};

export const MENU_THEME_PRESETS: Record<string, MenuThemeColors> = {
  default: DEFAULT_MENU_THEME,
  classic: {
    platinum: "#3db4d6",
    gold: "#ffd83b",
    silver: "#bfbfbf",
    bronze: "#ffad2b",
    bandaid: "#ff8073",
  },
  vibrant: {
    platinum: "#06b6d4",
    gold: "#fbbf24",
    silver: "#94a3b8",
    bronze: "#f97316",
    bandaid: "#ef4444",
  },
  professional: {
    platinum: "#0ea5e9",
    gold: "#eab308",
    silver: "#9ca3af",
    bronze: "#ea580c",
    bandaid: "#dc2626",
  },
  patriotic: {
    platinum: "#1e40af",
    gold: "#fbbf24",
    silver: "#e5e7eb",
    bronze: "#ea580c",
    bandaid: "#dc2626",
  },
  earth: {
    platinum: "#0891b2",
    gold: "#ca8a04",
    silver: "#78716c",
    bronze: "#92400e",
    bandaid: "#991b1b",
  },
};

// Shared state across all useTheme() calls
const sharedColors = ref<ThemeColors>({ ...DEFAULT_THEME });
const sharedMenuColors = ref<MenuThemeColors>({ ...DEFAULT_MENU_THEME });

// Convert hex to RGB
export const hexToRgb = (hex: string): { r: number; g: number; b: number } | null => {
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  if (!result) return null;

  return {
    r: parseInt(result[1], 16),
    g: parseInt(result[2], 16),
    b: parseInt(result[3], 16),
  };
};

// Convert RGB to hex
export const rgbToHex = (r: number, g: number, b: number): string => {
  return "#" + [r, g, b].map(x => {
    const hex = Math.round(x).toString(16);
    return hex.length === 1 ? "0" + hex : hex;
  }).join("");
};

// Calculate luminance for contrast determination
export const getLuminance = (hex: string): number => {
  const rgb = hexToRgb(hex);
  if (!rgb) return 0;

  const { r, g, b } = rgb;
  const [rs, gs, bs] = [r, g, b].map(c => {
    const s = c / 255;
    return s <= 0.03928 ? s / 12.92 : Math.pow((s + 0.055) / 1.055, 2.4);
  });

  return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
};

// Determine if text should be black or white based on background
export const getContrastColor = (hex: string): string => {
  const luminance = getLuminance(hex);
  return luminance > 0.5 ? "#000000" : "#ffffff";
};

// Adjust color brightness (positive = lighter, negative = darker)
export const adjustColor = (hex: string, percent: number): string => {
  const rgb = hexToRgb(hex);
  if (!rgb) return hex;

  const { r, g, b } = rgb;
  const amount = Math.round(2.55 * percent);

  const newR = Math.max(0, Math.min(255, r + amount));
  const newG = Math.max(0, Math.min(255, g + amount));
  const newB = Math.max(0, Math.min(255, b + amount));

  return rgbToHex(newR, newG, newB);
};

export const useTheme = () => {
  const sessionStore = useSessionStore();
  const preferences = usePreferencesStore();
  // Use shared refs instead of creating new ones
  const colors = sharedColors;
  const menuColors = sharedMenuColors;

  // Check if dark mode is currently active
  const isDarkMode = () => {
    return document.documentElement.classList.contains('ion-palette-dark');
  };

  // Apply a single color to CSS variables
  const applyColor = (colorName: keyof ColorSet, hexValue: string) => {
    const rgb = hexToRgb(hexValue);
    if (!rgb) return;

    const shade = adjustColor(hexValue, -12);
    const tint = adjustColor(hexValue, 12);
    const contrast = getContrastColor(hexValue);
    const contrastRgb = hexToRgb(contrast);

    // Update CSS variables
    document.documentElement.style.setProperty(
      `--ion-color-${colorName}`,
      hexValue
    );
    document.documentElement.style.setProperty(
      `--ion-color-${colorName}-rgb`,
      `${rgb.r},${rgb.g},${rgb.b}`
    );
    document.documentElement.style.setProperty(
      `--ion-color-${colorName}-shade`,
      shade
    );
    document.documentElement.style.setProperty(
      `--ion-color-${colorName}-tint`,
      tint
    );
    document.documentElement.style.setProperty(
      `--ion-color-${colorName}-contrast`,
      contrast
    );
    if (contrastRgb) {
      document.documentElement.style.setProperty(
        `--ion-color-${colorName}-contrast-rgb`,
        `${contrastRgb.r},${contrastRgb.g},${contrastRgb.b}`
      );
    }
  };

  // Apply a single menu color to CSS variables
  const applyMenuColor = (colorName: keyof MenuThemeColors, hexValue: string) => {
    const rgb = hexToRgb(hexValue);
    if (!rgb) return;

    const shade = adjustColor(hexValue, -12);
    const tint = adjustColor(hexValue, 12);

    // Update CSS variables for menu tier colors
    document.documentElement.style.setProperty(
      `--menu-color-${colorName}`,
      hexValue
    );
    document.documentElement.style.setProperty(
      `--menu-color-${colorName}-rgb`,
      `${rgb.r},${rgb.g},${rgb.b}`
    );
    document.documentElement.style.setProperty(
      `--menu-color-${colorName}-shade`,
      shade
    );
    document.documentElement.style.setProperty(
      `--menu-color-${colorName}-tint`,
      tint
    );
  };

  // Apply all colors from theme object
  const applyTheme = (theme: ThemeColors) => {
    // Validate theme structure before applying
    if (!theme || typeof theme !== 'object' || !theme.light || !theme.dark) {
      colors.value = { ...DEFAULT_THEME };
    } else {
      colors.value = { ...theme };
    }

    // Determine which color set to use based on current mode
    const colorSet = isDarkMode() ? colors.value.dark : colors.value.light;

    // Validate colorSet is an object
    if (!colorSet || typeof colorSet !== 'object') {
      const defaultColorSet = isDarkMode() ? DEFAULT_THEME.dark : DEFAULT_THEME.light;
      colors.value[isDarkMode() ? 'dark' : 'light'] = { ...defaultColorSet };
      Object.entries(defaultColorSet).forEach(([colorName, hexValue]) => {
        applyColor(colorName as keyof ColorSet, hexValue);
      });
      return;
    }

    // Apply each color from the appropriate set
    Object.entries(colorSet).forEach(([colorName, hexValue]) => {
      applyColor(colorName as keyof ColorSet, hexValue);
    });
  };

  // Apply all menu colors from menu theme object
  const applyMenuTheme = (theme: MenuThemeColors) => {
    menuColors.value = { ...theme };
    Object.entries(theme).forEach(([colorName, hexValue]) => {
      applyMenuColor(colorName as keyof MenuThemeColors, hexValue);
    });
  };

  // Set a single color and persist to store
  const setColor = async (colorName: keyof ColorSet, hexValue: string, mode: 'light' | 'dark') => {
    // Ensure colors.value has proper structure
    if (!colors.value || typeof colors.value !== 'object') {
      colors.value = { ...DEFAULT_THEME };
    }
    if (!colors.value[mode] || typeof colors.value[mode] !== 'object') {
      colors.value[mode] = { ...DEFAULT_THEME[mode] };
    }

    colors.value[mode][colorName] = hexValue;

    // Only apply if we're currently in that mode
    if ((mode === 'dark' && isDarkMode()) || (mode === 'light' && !isDarkMode())) {
      applyColor(colorName, hexValue);
    }

    // Session store theme is legacy - not using anymore
  };

  // Set a single menu color and persist to store
  const setMenuColor = async (colorName: keyof MenuThemeColors, hexValue: string) => {
    menuColors.value[colorName] = hexValue;
    applyMenuColor(colorName, hexValue);
    await sessionStore.updateMenuThemeColor(colorName, hexValue);
  };

  // Apply a preset theme
  const applyPreset = async (presetName: string) => {
    const preset = THEME_PRESETS[presetName];
    if (!preset) return;

    applyTheme(preset);
    // Session store theme is legacy - not saving there anymore
  };

  // Apply a preset menu theme
  const applyMenuPreset = async (presetName: string) => {
    const preset = MENU_THEME_PRESETS[presetName];
    if (!preset) return;

    applyMenuTheme(preset);
    await sessionStore.setMenuThemeColors(preset);
  };

  // Reset to default theme
  const resetTheme = async () => {
    applyTheme(DEFAULT_THEME);
    // Session store theme is legacy - not using anymore
  };

  // Reset to default menu theme
  const resetMenuTheme = async () => {
    applyMenuTheme(DEFAULT_MENU_THEME);
    await sessionStore.resetMenuTheme();
  };

  // Load theme from preferences or session store
  const loadTheme = async () => {
    const mode = isDarkMode() ? 'dark' : 'light';

    // Check for saved custom themes for both modes
    const lightThemeJson = await preferences.getPreference('customTheme-light');
    const darkThemeJson = await preferences.getPreference('customTheme-dark');

    // Check if we have valid (non-empty) custom themes
    const hasLightTheme = lightThemeJson && lightThemeJson.trim() !== '';
    const hasDarkTheme = darkThemeJson && darkThemeJson.trim() !== '';

    if (hasLightTheme || hasDarkTheme) {
      try {
        let lightColors = DEFAULT_THEME.light;
        let darkColors = DEFAULT_THEME.dark;

        if (hasLightTheme) {
          lightColors = JSON.parse(lightThemeJson) as ColorSet;
        }

        if (hasDarkTheme) {
          darkColors = JSON.parse(darkThemeJson) as ColorSet;
        }

        const customTheme: ThemeColors = {
          light: lightColors,
          dark: darkColors
        };

        applyTheme(customTheme);
      } catch (error) {
        // Fall back to session store or default
        loadThemeFromSessionStore();
      }
    } else {
      // No custom theme saved, load from session store or default
      loadThemeFromSessionStore();
    }

    // Load menu theme
    const savedMenuTheme = sessionStore.menuThemeColors;
    if (savedMenuTheme && Object.keys(savedMenuTheme).length > 0) {
      applyMenuTheme(savedMenuTheme);
    } else {
      applyMenuTheme(DEFAULT_MENU_THEME);
    }
  };

  // Helper to load theme from session store (legacy - not used anymore)
  const loadThemeFromSessionStore = () => {
    // Session store theme storage is legacy and incompatible with new theme structure
    // Just apply default theme
    applyTheme(DEFAULT_THEME);
  };

  // Re-apply current theme (useful when toggling dark/light mode)
  const reapplyTheme = () => {
    applyTheme(colors.value);
  };

  return {
    colors,
    menuColors,
    setColor,
    setMenuColor,
    applyPreset,
    applyMenuPreset,
    resetTheme,
    resetMenuTheme,
    loadTheme,
    applyTheme,
    applyMenuTheme,
    reapplyTheme,
    isDarkMode,
  };
};