Hello from MCP server

List Files | Just Commands | Repo | Logs

← back |
<template>
  <BaseLayout title="Theme Settings">
    <div class="theme-settings-container">
      <div class="header-section">
        <div class="title-container">
          <div class="title-icons">
            <ion-icon :icon="arrowBack" @click="goBack" class="clickable-icon"></ion-icon>
          </div>
          <h1 class="page-title">Theme Customizer</h1>
        </div>
      </div>

      <div class="content-section">
        <!-- Theme Presets -->
        <div class="presets-section">
          <h2 class="section-title">Quick Themes</h2>
          <div class="presets-grid">
            <div
              v-for="(preset, name) in presets"
              :key="name"
              @click="applyPreset(name)"
              class="preset-card"
            >
              <div class="preset-colors">
                <div
                  class="preset-color"
                  v-for="(color, colorName) in getPresetColors(preset)"
                  :key="colorName"
                  :style="{ backgroundColor: color }"
                ></div>
              </div>
              <p class="preset-name">{{ formatPresetName(name) }}</p>
            </div>
          </div>
        </div>

        <div class="divider"></div>

        <!-- Custom Colors -->
        <div class="colors-section">
          <h2 class="section-title">Custom Colors</h2>
          <div class="colors-grid">
            <div
              v-for="(value, name) in currentModeColors"
              :key="name"
              class="color-setting"
            >
              <div class="color-header">
                <label class="color-label">{{ formatColorName(name as string) }}</label>
                <span class="color-value">{{ value }}</span>
              </div>
              <div class="color-input-wrapper">
                <input
                  type="color"
                  :value="value"
                  @input="handleColorChange(name as keyof ColorSet, ($event.target as HTMLInputElement).value)"
                  class="color-picker"
                />
                <div class="color-preview" :style="{ backgroundColor: value as string }">
                  <span
                    class="preview-text"
                    :style="{ color: getContrastColor(value as string) }"
                  >
                    Aa
                  </span>
                </div>
              </div>
            </div>
          </div>
        </div>

        <div class="divider"></div>

        <!-- Preview Section -->
        <div class="preview-section">
          <h2 class="section-title">Preview</h2>
          <div class="preview-content">
            <ion-button color="primary">Primary Button</ion-button>
            <ion-button color="secondary">Secondary Button</ion-button>
            <ion-button color="tertiary">Tertiary Button</ion-button>
            <ion-button color="success">Success Button</ion-button>
            <ion-button color="warning">Warning Button</ion-button>
            <ion-button color="danger">Danger Button</ion-button>
          </div>
        </div>

        <div class="divider"></div>

        <!-- Menu Theme Presets -->
        <div class="presets-section">
          <h2 class="section-title">Menu Themes</h2>
          <p class="section-subtitle">Choose colors for menu tier badges (independent from app theme)</p>
          <div class="presets-grid">
            <div
              v-for="(preset, name) in menuPresets"
              :key="name"
              @click="applyMenuPreset(name)"
              class="preset-card"
            >
              <div class="preset-colors">
                <div
                  class="preset-color"
                  v-for="(color, colorName) in preset"
                  :key="colorName"
                  :style="{ backgroundColor: color }"
                ></div>
              </div>
              <p class="preset-name">{{ formatPresetName(name) }}</p>
            </div>
          </div>
        </div>

        <div class="divider"></div>

        <!-- Menu Custom Colors -->
        <div class="colors-section">
          <h2 class="section-title">Custom Menu Colors</h2>
          <div class="colors-grid">
            <div
              v-for="(value, name) in menuColors"
              :key="name"
              class="color-setting"
            >
              <div class="color-header">
                <label class="color-label">{{ formatColorName(name) }}</label>
                <span class="color-value">{{ value }}</span>
              </div>
              <div class="color-input-wrapper">
                <input
                  type="color"
                  :value="value"
                  @input="handleMenuColorChange(name as keyof MenuThemeColors, ($event.target as HTMLInputElement).value)"
                  class="color-picker"
                />
                <div class="color-preview" :style="{ backgroundColor: value }">
                  <span
                    class="preview-text"
                    :style="{ color: getContrastColor(value) }"
                  >
                    Aa
                  </span>
                </div>
              </div>
            </div>
          </div>
        </div>

        <div class="divider"></div>

        <!-- Actions -->
        <div class="actions-section">
          <ion-button
            @click="handleReset"
            fill="outline"
            color="danger"
            size="large"
          >
            <ion-icon :icon="refreshOutline" slot="start"></ion-icon>
            Reset App Theme
          </ion-button>
          <ion-button
            @click="handleMenuReset"
            fill="outline"
            color="warning"
            size="large"
          >
            <ion-icon :icon="refreshOutline" slot="start"></ion-icon>
            Reset Menu Theme
          </ion-button>
        </div>
      </div>
    </div>
  </BaseLayout>
</template>

<script setup lang="ts">
import { onMounted } from "vue";
import { useRouter } from "vue-router";
import BaseLayout from "@/components/BaseLayout.vue";
import { IonIcon, IonButton } from "@ionic/vue";
import { arrowBack, refreshOutline } from "ionicons/icons";
import { useTheme, THEME_PRESETS, MENU_THEME_PRESETS, type ColorSet, type MenuThemeColors, getContrastColor as getContrast } from "@/composables/useTheme";

import { computed } from 'vue';

const router = useRouter();
const { colors, menuColors, setColor, setMenuColor, applyPreset: applyThemePreset, applyMenuPreset: applyThemeMenuPreset, resetTheme, resetMenuTheme, loadTheme } = useTheme();

const presets = THEME_PRESETS;
const menuPresets = MENU_THEME_PRESETS;

// Get current mode's colors
const currentModeColors = computed(() => {
  const mode = document.documentElement.classList.contains('ion-palette-dark') ? 'dark' : 'light';
  return colors.value?.[mode] || {};
});

const goBack = () => {
  router.back();
};

const formatColorName = (name: string): string => {
  return name.charAt(0).toUpperCase() + name.slice(1);
};

const formatPresetName = (name: string): string => {
  return name.charAt(0).toUpperCase() + name.slice(1);
};

const getPresetColors = (preset: any) => {
  // preset is a ThemeColors with light/dark, get the light mode colors for preview
  const colorSet = preset.light || preset;
  return {
    primary: colorSet.primary,
    secondary: colorSet.secondary,
    tertiary: colorSet.tertiary,
  };
};

const handleColorChange = async (colorName: keyof ColorSet, hexValue: string) => {
  // Get current mode
  const mode = document.documentElement.classList.contains('ion-palette-dark') ? 'dark' : 'light';
  await setColor(colorName, hexValue, mode);
};

const handleMenuColorChange = async (colorName: keyof MenuThemeColors, hexValue: string) => {
  await setMenuColor(colorName, hexValue);
};

const applyPreset = async (presetName: string) => {
  await applyThemePreset(presetName);
};

const applyMenuPreset = async (presetName: string) => {
  await applyThemeMenuPreset(presetName);
};

const handleReset = async () => {
  if (confirm("Are you sure you want to reset app theme colors to their default values?")) {
    await resetTheme();
  }
};

const handleMenuReset = async () => {
  if (confirm("Are you sure you want to reset menu theme colors to their default values?")) {
    await resetMenuTheme();
  }
};

const getContrastColor = (hex: string): string => {
  return getContrast(hex);
};

onMounted(() => {
  loadTheme();
});
</script>

<style scoped>
.theme-settings-container {
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
}

.header-section {
  margin-bottom: 32px;
}

.title-container {
  position: relative;
  margin-bottom: 24px;
}

.title-icons {
  position: absolute;
  left: 12px;
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  gap: 16px;
}

.title-icons ion-icon {
  font-size: 48px;
}

.clickable-icon {
  cursor: pointer;
  transition: transform 0.2s ease, color 0.2s ease;
}

.clickable-icon:hover {
  transform: scale(1.1);
  color: var(--ion-color-primary-shade);
}

.clickable-icon:active {
  transform: scale(0.95);
}

.page-title {
  margin: 0;
  color: #ffffff;
  font-size: 48px;
  font-weight: 700;
  text-align: center;
  font-variant: small-caps;
}

.content-section {
  padding: 0 20px 20px 20px;
}

.section-title {
  margin: 0 0 24px 0;
  color: #ffffff;
  font-size: 32px;
  font-weight: 600;
  font-variant: small-caps;
}

.section-subtitle {
  margin: -16px 0 16px 0;
  color: var(--ion-color-medium);
  font-size: 16px;
  font-weight: 400;
}

.divider {
  width: 100%;
  height: 1px;
  background-color: #d3d3d3;
  margin: 40px 0;
}

/* Presets Section */
.presets-section {
  margin-bottom: 32px;
}

.presets-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
}

.preset-card {
  background-color: var(--ion-color-dark);
  border-radius: 12px;
  padding: 20px;
  cursor: pointer;
  transition: transform 0.2s ease, box-shadow 0.2s ease;
  border: 2px solid transparent;
}

.preset-card:hover {
  transform: translateY(-4px);
  box-shadow: 0 8px 16px rgba(118, 221, 84, 0.2);
  border-color: var(--ion-color-primary);
}

.preset-colors {
  display: flex;
  gap: 8px;
  margin-bottom: 12px;
  height: 60px;
}

.preset-color {
  flex: 1;
  border-radius: 8px;
  transition: transform 0.2s ease;
}

.preset-card:hover .preset-color {
  transform: scale(1.05);
}

.preset-name {
  margin: 0;
  color: #ffffff;
  font-size: 18px;
  font-weight: 600;
  text-align: center;
  font-variant: small-caps;
}

/* Colors Section */
.colors-section {
  margin-bottom: 32px;
}

.colors-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  gap: 24px;
}

.color-setting {
  background-color: var(--ion-color-dark);
  border-radius: 12px;
  padding: 20px;
  transition: transform 0.2s ease;
}

.color-setting:hover {
  transform: translateY(-2px);
}

.color-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 12px;
}

.color-label {
  color: #ffffff;
  font-size: 18px;
  font-weight: 600;
  font-variant: small-caps;
}

.color-value {
  color: var(--ion-color-medium);
  font-size: 14px;
  font-family: "Courier New", Courier, monospace;
  text-transform: uppercase;
}

.color-input-wrapper {
  display: flex;
  gap: 12px;
  align-items: center;
}

.color-picker {
  width: 80px;
  height: 80px;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: transform 0.2s ease;
}

.color-picker:hover {
  transform: scale(1.05);
}

.color-preview {
  flex: 1;
  height: 80px;
  border-radius: 8px;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 2px solid var(--ion-color-medium);
  transition: border-color 0.2s ease;
}

.color-setting:hover .color-preview {
  border-color: var(--ion-color-primary);
}

.preview-text {
  font-size: 32px;
  font-weight: 700;
  user-select: none;
}

/* Preview Section */
.preview-section {
  margin-bottom: 32px;
}

.preview-content {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  padding: 24px;
  background-color: var(--ion-color-dark);
  border-radius: 12px;
}

/* Actions Section */
.actions-section {
  display: flex;
  justify-content: center;
  gap: 16px;
}

/* Mobile adjustments */
@media (max-width: 767px) {
  .theme-settings-container {
    padding: 16px;
  }

  .page-title {
    font-size: 36px;
  }

  .section-title {
    font-size: 24px;
  }

  .presets-grid {
    grid-template-columns: 1fr;
  }

  .colors-grid {
    grid-template-columns: 1fr;
  }

  .preview-content {
    flex-direction: column;
  }

  .actions-section {
    flex-direction: column;
  }
}
</style>