Hello from MCP server
<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>