Hello from MCP server
<template>
<BaseLayout title="Theme Designer">
<div class="theme-designer-container">
<!-- Header Section -->
<div class="header-section">
<h1 class="preview-title">Choose your palette</h1>
<button class="preview-toggle-button" @click="togglePreview">
{{ showComponentPreview ? 'Hide Components' : 'Preview Components' }}
</button>
</div>
<!-- Component Preview Section -->
<div v-if="showComponentPreview" class="preview-section">
<!-- Toolbar Component -->
<div class="component-preview">
<h3 class="component-heading">Technician Toolbar</h3>
<div class="toolbar-preview-wrapper">
<Toolbar
:force-step1="true"
:force-step2="true"
:force-step3="true"
/>
</div>
</div>
<!-- Search Bar Component -->
<div class="component-preview">
<h3 class="component-heading">Search Bar</h3>
<SearchBar
title="What are we doing today?"
:search-terms="mockSearchTerms"
:selected-tags="mockSelectedTags"
:available-tags="mockAvailableTags"
:suggestions="[]"
button-text="Find Job"
navigate-path="/find-job"
@navigate="() => {}"
/>
</div>
<!-- Search Result Component -->
<div class="component-preview">
<h3 class="component-heading">Search Result</h3>
<div class="search-results">
<SearchResult
:title="mockProblem.name"
:description="mockProblem.description"
@click="() => {}"
/>
</div>
</div>
<!-- Yes and No Buttons -->
<div class="component-preview">
<h3 class="component-heading">Yes Button (Enabled)</h3>
<YesButton text="Yes" @click="() => {}" />
</div>
<div class="component-preview">
<h3 class="component-heading">Yes Button (Disabled)</h3>
<YesButton text="Yes" :disabled="true" @click="() => {}" />
</div>
<div class="component-preview">
<h3 class="component-heading">No Button (Enabled)</h3>
<NoButton text="No" @click="() => {}" />
</div>
<div class="component-preview">
<h3 class="component-heading">No Button (Disabled)</h3>
<NoButton text="No" :disabled="true" @click="() => {}" />
</div>
<!-- Job Description Confirmation Section -->
<div class="component-preview">
<h3 class="component-heading">Confirmation Section</h3>
<ConfirmationSection :confirmed="mockConfirmed" @confirm="toggleMockConfirmed">
<h3 class="section-heading">Job Description</h3>
<h4 class="job-name">{{ mockProblem.name }}</h4>
<div class="description" v-html="mockProblem.description"></div>
</ConfirmationSection>
</div>
<!-- Check Hours Buttons -->
<div class="component-preview">
<h3 class="component-heading">Check Hours Buttons</h3>
<button class="skip-button">Skip Checklist</button>
<div class="button-group">
<button class="option-button yes-button">Yes</button>
<button class="option-button no-button">No</button>
</div>
</div>
<!-- Job Listing -->
<div class="component-preview">
<h3 class="component-heading">Job Listing</h3>
<DashboardJob
title="Fixture Drain Cleaning Interior"
hours="2.5 h"
:has-confirmed-offer="false"
@delete="() => {}"
@edit="() => {}"
/>
</div>
<!-- Job Listing (With Confirmed Offer) -->
<div class="component-preview">
<h3 class="component-heading">Job Listing (Ready to Copy)</h3>
<DashboardJob
title="Fixture Drain Cleaning Interior"
hours="2.5 h"
:has-confirmed-offer="true"
:job-data="mockJobData"
@copied="() => {}"
/>
</div>
<!-- Notification Bubble -->
<div class="component-preview">
<h3 class="component-heading">Notification Bubble</h3>
<div class="notification-bubbles">
<div class="notification-bubble">
<ion-icon :icon="warning" class="notification-icon"></ion-icon>
<span class="notification-text">I can do the job without using a ladder.</span>
</div>
<div class="notification-bubble">
<ion-icon :icon="warning" class="notification-icon"></ion-icon>
<span class="notification-text">I can do the job without accessing the roof.</span>
</div>
</div>
</div>
<!-- Divider -->
<div class="component-preview">
<h3 class="component-heading">Divider</h3>
<Divider />
</div>
</div>
<!-- Logo Upload Section -->
<div class="logo-upload-section">
<h2 class="section-title-main">Custom Logos</h2>
<!-- Logo Square Upload -->
<div class="logo-upload-card">
<h3 class="logo-heading">Square Logo (App Bar)</h3>
<p class="logo-description">Upload a square logo to replace the default logo in the app bar. Recommended size: 48x48px or larger.</p>
<div class="logo-preview-container">
<div v-if="currentLogoSquare" class="logo-preview">
<img :src="currentLogoSquare" alt="Square Logo Preview" class="logo-image logo-image-square" />
</div>
<div v-else class="logo-preview logo-preview-empty">
<span>No logo uploaded</span>
</div>
</div>
<div class="logo-actions">
<label class="upload-button">
<input
type="file"
accept="image/*"
@change="handleLogoUpload($event, 'logo-square')"
class="file-input-hidden"
/>
<ion-icon :icon="cloudUploadOutline" slot="start"></ion-icon>
Upload Square Logo
</label>
<button
v-if="currentLogoSquare"
class="remove-button"
@click="handleLogoRemove('logo-square')"
>
<ion-icon :icon="trashOutline"></ion-icon>
Remove
</button>
</div>
</div>
<!-- Full Logo Upload -->
<div class="logo-upload-card">
<h3 class="logo-heading">Full Logo (Sidebar)</h3>
<p class="logo-description">Upload a full logo to replace the default logo when the sidebar navigation is opened. Recommended width: 200-300px.</p>
<div class="logo-preview-container">
<div v-if="currentLogo" class="logo-preview">
<img :src="currentLogo" alt="Full Logo Preview" class="logo-image logo-image-full" />
</div>
<div v-else class="logo-preview logo-preview-empty">
<span>No logo uploaded</span>
</div>
</div>
<div class="logo-actions">
<label class="upload-button">
<input
type="file"
accept="image/*"
@change="handleLogoUpload($event, 'logo')"
class="file-input-hidden"
/>
<ion-icon :icon="cloudUploadOutline" slot="start"></ion-icon>
Upload Full Logo
</label>
<button
v-if="currentLogo"
class="remove-button"
@click="handleLogoRemove('logo')"
>
<ion-icon :icon="trashOutline"></ion-icon>
Remove
</button>
</div>
</div>
</div>
<!-- Fixed Palette Bar -->
<div class="palette-bar">
<!-- Color Labels (positioned outside the palette container) -->
<div class="color-labels">
<span
v-for="colorName in colorNames"
:key="colorName + '-label'"
class="color-label"
>{{ colorName }}</span>
</div>
<!-- Color Circles -->
<div class="color-circles">
<div
v-for="colorName in colorNames"
:key="colorName"
class="color-circle"
:style="{ backgroundColor: getCurrentColor(colorName) }"
@click="openColorPickerModal(colorName)"
>
</div>
</div>
<!-- Dark Mode Toggle -->
<div class="palette-action" @click="toggleDarkMode">
<ion-icon :icon="isDark ? sunnyOutline : moonOutline" size="large"></ion-icon>
</div>
<!-- Edit Icon -->
<div class="palette-action" @click="openJsonEditor">
<ion-icon :icon="createOutline" size="large"></ion-icon>
</div>
<!-- Reset to Default Button -->
<div class="palette-action palette-action-danger" @click="showResetConfirmation">
<ion-icon :icon="refreshOutline" size="large"></ion-icon>
</div>
</div>
<!-- Reset Confirmation Alert -->
<ion-alert
:is-open="isResetAlertOpen"
header="Reset Theme"
message="Are you sure you want to reset the theme to default? This will discard all your custom changes."
:buttons="[
{
text: 'Cancel',
role: 'cancel',
handler: cancelReset
},
{
text: 'Reset',
role: 'destructive',
handler: confirmReset
}
]"
@didDismiss="cancelReset"
></ion-alert>
<!-- Color Picker Modal -->
<ion-modal :is-open="isColorPickerOpen" @didDismiss="closeColorPickerModal" :initial-breakpoint="0.5" :breakpoints="[0, 0.5, 0.75]">
<ion-header>
<ion-toolbar>
<ion-title>{{ activeColorName }}</ion-title>
<ion-buttons slot="end">
<ion-button @click="closeColorPickerModal">Done</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="color-picker-content">
<hex-color-picker
:color="activeColorValue"
@color-changed="handleColorPickerChange"
></hex-color-picker>
<div class="color-preview-row">
<div class="color-preview-swatch" :style="{ backgroundColor: activeColorValue }"></div>
<input
type="text"
v-model="activeColorValue"
@change="applyColorFromInput"
class="color-hex-input"
maxlength="7"
/>
</div>
</div>
</ion-content>
</ion-modal>
<!-- Theme Editor Modal -->
<ion-modal :is-open="isJsonEditorOpen" @didDismiss="closeJsonEditor">
<ion-header>
<ion-toolbar>
<ion-title>Edit Theme</ion-title>
<ion-buttons slot="end">
<ion-button @click="closeJsonEditor">Close</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="theme-editor-content">
<!-- Color Fields -->
<div class="color-fields-grid">
<div
v-for="colorName in colorNames"
:key="colorName"
class="color-field-row"
>
<div
class="color-field-swatch"
:style="{ backgroundColor: editableColors[colorName] }"
@click="openInlineColorPicker(colorName)"
></div>
<label class="color-field-label">{{ colorName }}</label>
<input
type="text"
:value="editableColors[colorName]"
@input="updateEditableColor(colorName, ($event.target as HTMLInputElement).value)"
class="color-field-input"
maxlength="7"
placeholder="#000000"
/>
</div>
</div>
<div class="theme-editor-actions">
<ion-button @click="copyThemeJson" fill="outline">
<ion-icon :icon="copyOutline" slot="start"></ion-icon>
Copy JSON
</ion-button>
<ion-button @click="pasteThemeJson" fill="outline">
<ion-icon :icon="clipboardOutline" slot="start"></ion-icon>
Paste JSON
</ion-button>
<ion-button @click="applyEditableColors" color="primary">
Apply Theme
</ion-button>
<ion-button @click="saveThemePreference" color="success">
Save Theme
</ion-button>
</div>
</div>
</ion-content>
</ion-modal>
</div>
</BaseLayout>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import {
IonButton,
IonIcon,
IonModal,
IonHeader,
IonToolbar,
IonTitle,
IonButtons,
IonContent,
IonAlert,
toastController,
} from '@ionic/vue';
import { sunnyOutline, moonOutline, createOutline, warning, refreshOutline, cloudUploadOutline, trashOutline, copyOutline, clipboardOutline } from 'ionicons/icons';
import 'vanilla-colorful/hex-color-picker.js';
import BaseLayout from '@/components/BaseLayout.vue';
import Toolbar from '@/components/Toolbar.vue';
import SearchBar from '@/components/SearchBar.vue';
import SearchResult from '@/components/SearchResult.vue';
import YesButton from '@/components/YesButton.vue';
import NoButton from '@/components/NoButton.vue';
import ConfirmationSection from '@/components/ConfirmationSection.vue';
import Divider from '@/components/Divider.vue';
import DashboardJob from '@/components/DashboardJob.vue';
import { useTheme, hexToRgb } from '@/composables/useTheme';
import { usePreferencesStore } from '@/stores/preferences';
import { useSessionStore } from '@/stores/session';
import { useLogo } from '@/composables/useLogo';
import { fileToDataUrl, type LogoType } from '@/lib/logoStorage';
const { colors, setColor, isDarkMode, reapplyTheme } = useTheme();
const preferences = usePreferencesStore();
const sessionStore = useSessionStore();
const { logoSquare, logo, loadLogos, uploadLogo, removeLogo } = useLogo();
const isJsonEditorOpen = ref(false);
const jsonTheme = ref('');
const editableColors = ref<Record<string, string>>({});
const mockConfirmed = ref(false);
const isResetAlertOpen = ref(false);
const currentLogoSquare = ref<string | null>(null);
const currentLogo = ref<string | null>(null);
const showComponentPreview = ref(false);
// Color picker modal state
const isColorPickerOpen = ref(false);
const activeColorName = ref('');
const activeColorValue = ref('#000000');
// Set up mock progress state for toolbar preview
sessionStore.appProgress.step1 = true; // Complete with checkmark
sessionStore.appProgress.step2 = true; // Complete with checkmark
sessionStore.appProgress.step3 = false; // Will be forced filled without checkmark
sessionStore.appProgress.step4 = 'pending'; // Warning color
sessionStore.appProgress.step5 = false; // Empty outline
const colorNames = ['primary', 'secondary', 'tertiary', 'success', 'warning', 'danger', 'light', 'medium', 'dark'] as const;
// Mock data for preview
const mockSearchTerms = ref<string[]>(['drain']);
const mockSelectedTags = ref<Array<{ id: string; name: string }>>([
{ id: '1', name: 'plumbing' }
]);
const mockAvailableTags = ref<Array<{ id: string; name: string; count: number }>>([
{ id: '2', name: 'interior', count: 45 },
{ id: '3', name: 'fixture', count: 32 },
{ id: '4', name: 'cleaning', count: 28 },
{ id: '5', name: 'sink', count: 24 },
{ id: '6', name: 'tub', count: 18 },
]);
const mockProblem = {
name: 'Fixture Drain Cleaning Interior',
description: `Clear a clogged drain in a sink, tub, or shower.
Service includes:
- Snake drain line
- Clear blockage
- Test drainage
- Clean work area
#plumbing #drain #fixture #interior`
};
const mockJobData = {
title: 'Fixture Drain Cleaning Interior',
problem: {
name: 'Fixture Drain Cleaning Interior',
description: 'Clear a clogged drain in a sink, tub, or shower.'
},
selectedTierTitle: 'Premium Drain Cleaning Service',
selectedTierName: 'Gold',
selectedPrice: '285.00',
baseHours: 2,
extraTime: 1.25,
notes: ['Customer mentioned slow drainage for past week', 'Kitchen sink affected']
};
const isDark = computed(() => preferences.darkMode);
const getCurrentColor = (colorName: string) => {
const mode = isDark.value ? 'dark' : 'light';
const colorSet = colors.value[mode];
if (!colorSet || typeof colorSet !== 'object') return '#000000';
return colorSet[colorName as keyof typeof colorSet] || '#000000';
};
const openColorPickerModal = (colorName: string) => {
activeColorName.value = colorName;
activeColorValue.value = getCurrentColor(colorName);
isColorPickerOpen.value = true;
};
const closeColorPickerModal = () => {
isColorPickerOpen.value = false;
};
const handleColorPickerChange = async (event: CustomEvent) => {
const newColor = event.detail.value;
activeColorValue.value = newColor;
const mode = isDark.value ? 'dark' : 'light';
await setColor(activeColorName.value as any, newColor, mode);
// Also update editableColors if the theme editor is open
if (isJsonEditorOpen.value && activeColorName.value) {
editableColors.value[activeColorName.value] = newColor;
}
};
const applyColorFromInput = async () => {
// Validate hex format
if (/^#[0-9A-Fa-f]{6}$/.test(activeColorValue.value)) {
const mode = isDark.value ? 'dark' : 'light';
await setColor(activeColorName.value as any, activeColorValue.value, mode);
}
};
const toggleDarkMode = async () => {
await preferences.setPreference('darkMode', String(!preferences.darkMode));
};
const openJsonEditor = () => {
const mode = isDark.value ? 'dark' : 'light';
// Ensure we have a valid colors object
if (!colors.value || !colors.value[mode]) {
console.error('[ThemeDesigner] Colors not properly initialized');
// Import default theme as fallback
import('@/composables/useTheme').then(({ DEFAULT_THEME }) => {
jsonTheme.value = JSON.stringify(DEFAULT_THEME[mode], null, 2);
// Populate editable colors from default theme
editableColors.value = { ...DEFAULT_THEME[mode] };
isJsonEditorOpen.value = true;
});
return;
}
jsonTheme.value = JSON.stringify(colors.value[mode], null, 2);
// Populate editable colors from current theme
editableColors.value = { ...colors.value[mode] };
isJsonEditorOpen.value = true;
};
const closeJsonEditor = () => {
isJsonEditorOpen.value = false;
};
// Update a single editable color
const updateEditableColor = (colorName: string, value: string) => {
editableColors.value[colorName] = value;
};
// Copy theme JSON to clipboard
const copyThemeJson = async () => {
const json = JSON.stringify(editableColors.value, null, 2);
try {
await navigator.clipboard.writeText(json);
const toast = await toastController.create({
message: 'Theme JSON copied to clipboard',
duration: 2000,
color: 'success',
});
await toast.present();
} catch (error) {
// Fallback for browsers that don't support clipboard API
const textarea = document.createElement('textarea');
textarea.value = json;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
const toast = await toastController.create({
message: 'Theme JSON copied to clipboard',
duration: 2000,
color: 'success',
});
await toast.present();
}
};
// Paste theme JSON from clipboard
const pasteThemeJson = async () => {
try {
const clipboardText = await navigator.clipboard.readText();
const parsed = JSON.parse(clipboardText);
// Validate that it has valid color values
let validCount = 0;
for (const colorName of colorNames) {
if (parsed[colorName] && /^#[0-9A-Fa-f]{6}$/i.test(parsed[colorName])) {
editableColors.value[colorName] = parsed[colorName].toUpperCase();
validCount++;
}
}
if (validCount > 0) {
const toast = await toastController.create({
message: `Updated ${validCount} color${validCount > 1 ? 's' : ''} from clipboard`,
duration: 2000,
color: 'success',
});
await toast.present();
} else {
const toast = await toastController.create({
message: 'No valid colors found in clipboard',
duration: 3000,
color: 'warning',
});
await toast.present();
}
} catch (error) {
const toast = await toastController.create({
message: 'Could not read clipboard or invalid JSON format',
duration: 3000,
color: 'danger',
});
await toast.present();
}
};
// Apply editable colors to theme
const applyEditableColors = async () => {
const mode = isDark.value ? 'dark' : 'light';
for (const [colorName, hexValue] of Object.entries(editableColors.value)) {
if (colorNames.includes(colorName as any) && /^#[0-9A-Fa-f]{6}$/.test(hexValue)) {
await setColor(colorName as any, hexValue, mode);
}
}
closeJsonEditor();
};
// Open inline color picker from the theme editor modal
const openInlineColorPicker = (colorName: string) => {
activeColorName.value = colorName;
activeColorValue.value = editableColors.value[colorName] || '#000000';
isColorPickerOpen.value = true;
};
const applyJsonTheme = async () => {
try {
const parsed = JSON.parse(jsonTheme.value);
const mode = isDark.value ? 'dark' : 'light';
// Apply each color
for (const [colorName, hexValue] of Object.entries(parsed)) {
if (colorNames.includes(colorName as any)) {
await setColor(colorName as any, hexValue as string, mode);
}
}
closeJsonEditor();
} catch (error) {
alert('Invalid JSON format');
}
};
const toggleMockConfirmed = () => {
mockConfirmed.value = !mockConfirmed.value;
};
const togglePreview = () => {
showComponentPreview.value = !showComponentPreview.value;
};
const saveThemePreference = async () => {
try {
const mode = isDark.value ? 'dark' : 'light';
const currentTheme = colors.value[mode];
console.log('[ThemeDesigner] Saving theme for mode:', mode);
console.log('[ThemeDesigner] Theme to save:', currentTheme);
// Save the current theme as a preference with mode identifier
await preferences.setPreference(`customTheme-${mode}`, JSON.stringify(currentTheme));
console.log('[ThemeDesigner] Theme saved successfully');
alert('Theme saved successfully!');
} catch (error) {
alert('Failed to save theme');
console.error('[ThemeDesigner] Error saving theme:', error);
}
};
const showResetConfirmation = () => {
isResetAlertOpen.value = true;
};
const cancelReset = () => {
isResetAlertOpen.value = false;
};
const confirmReset = async () => {
try {
const mode = isDark.value ? 'dark' : 'light';
// Remove the custom theme preference
await preferences.setPreference(`customTheme-${mode}`, '');
// Import and apply default theme for the current mode
const { applyTheme } = useTheme();
const { DEFAULT_THEME } = await import('@/composables/useTheme');
applyTheme(DEFAULT_THEME);
// Update the JSON editor to show the default theme
jsonTheme.value = JSON.stringify(DEFAULT_THEME[mode], null, 2);
console.log('[ThemeDesigner] Theme reset to default');
alert('Theme reset to default successfully!');
} catch (error) {
alert('Failed to reset theme');
console.error('[ThemeDesigner] Error resetting theme:', error);
} finally {
isResetAlertOpen.value = false;
}
};
// Logo upload handlers
const handleLogoUpload = async (event: Event, type: LogoType) => {
const input = event.target as HTMLInputElement;
const file = input.files?.[0];
if (!file) return;
// Validate file type
if (!file.type.startsWith('image/')) {
alert('Please upload an image file');
return;
}
// Validate file size (max 5MB)
if (file.size > 5 * 1024 * 1024) {
alert('Image file size must be less than 5MB');
return;
}
try {
const dataUrl = await fileToDataUrl(file);
await uploadLogo(type, dataUrl);
// Update local preview
if (type === 'logo-square') {
currentLogoSquare.value = dataUrl;
} else {
currentLogo.value = dataUrl;
}
console.log(`[ThemeDesigner] Successfully uploaded ${type}`);
} catch (error) {
console.error(`[ThemeDesigner] Error uploading ${type}:`, error);
alert('Failed to upload logo');
}
// Reset input so same file can be uploaded again
input.value = '';
};
const handleLogoRemove = async (type: LogoType) => {
try {
await removeLogo(type);
// Update local preview
if (type === 'logo-square') {
currentLogoSquare.value = null;
} else {
currentLogo.value = null;
}
console.log(`[ThemeDesigner] Successfully removed ${type}`);
} catch (error) {
console.error(`[ThemeDesigner] Error removing ${type}:`, error);
alert('Failed to remove logo');
}
};
// Load logos and ensure theme is initialized on mount
onMounted(async () => {
// Load logos
await loadLogos();
currentLogoSquare.value = logoSquare.value;
currentLogo.value = logo.value;
// Ensure colors are properly initialized
const mode = isDark.value ? 'dark' : 'light';
console.log('[ThemeDesigner] Mounted - checking colors:', colors.value);
// If colors aren't properly loaded, force a theme reload
if (!colors.value || !colors.value[mode] || typeof colors.value[mode] === 'string') {
console.warn('[ThemeDesigner] Colors not properly initialized, reloading theme');
const { loadTheme } = useTheme();
await loadTheme();
}
});
</script>
<style scoped>
.theme-designer-container {
padding: 20px;
padding-right: 120px; /* Make room for palette bar */
max-width: 1200px;
margin: 0 auto;
}
.header-section {
text-align: center;
margin-bottom: 32px;
}
.preview-toggle-button {
padding: 12px 32px;
font-size: 16px;
font-weight: 600;
color: var(--ion-color-primary-contrast);
background-color: var(--ion-color-primary);
border: 2px solid var(--ion-color-primary);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.preview-toggle-button:hover {
transform: scale(1.05);
background-color: var(--ion-color-primary-shade);
border-color: var(--ion-color-primary-shade);
}
.preview-toggle-button:active {
transform: scale(0.95);
}
.preview-section {
padding-bottom: 40px;
}
.preview-title {
margin: 0 0 32px 0;
color: var(--ion-text-color);
font-size: 36px;
font-weight: 700;
text-align: center;
}
.component-preview {
margin-bottom: 48px;
padding: 24px;
background: var(--ion-background-color);
border: 2px solid rgba(var(--ion-color-primary-rgb), 0.2);
border-radius: 12px;
}
.component-heading {
margin: 0 0 24px 0;
color: var(--ion-color-primary);
font-size: 24px;
font-weight: 600;
text-align: center;
}
.toolbar-preview-wrapper {
margin: -24px -24px 0 -24px;
border-radius: 12px 12px 0 0;
overflow: hidden;
}
/* Palette Bar Styles */
.palette-bar {
position: fixed;
top: 80px;
right: 20px;
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
background: rgba(var(--ion-background-color-rgb, 255, 255, 255), 0.95);
border: 2px solid var(--ion-color-primary);
border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
z-index: 1000;
backdrop-filter: blur(10px);
}
.color-labels {
position: absolute;
left: -90px;
top: 16px;
display: flex;
flex-direction: column;
gap: 12px;
align-items: flex-end;
}
.color-label {
height: 48px;
display: flex;
align-items: center;
padding: 6px 12px;
background: var(--ion-color-dark);
color: var(--ion-color-dark-contrast);
font-size: 12px;
font-weight: 600;
text-transform: capitalize;
border-radius: 6px 0 0 6px;
white-space: nowrap;
box-shadow: -2px 2px 8px rgba(0, 0, 0, 0.3);
}
.color-circles {
display: flex;
flex-direction: column;
gap: 12px;
}
.color-circle {
width: 48px;
height: 48px;
border-radius: 50%;
cursor: pointer;
border: 3px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
transition: transform 0.2s ease, box-shadow 0.2s ease;
flex-shrink: 0;
}
.color-circle:hover {
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
}
/* Color Picker Modal Styles */
.color-picker-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
padding: 20px;
}
.color-picker-content hex-color-picker {
width: 100%;
max-width: 300px;
height: 200px;
}
.color-preview-row {
display: flex;
align-items: center;
gap: 16px;
width: 100%;
max-width: 300px;
}
.color-preview-swatch {
width: 48px;
height: 48px;
border-radius: 8px;
border: 2px solid var(--ion-color-medium);
flex-shrink: 0;
}
.color-hex-input {
flex: 1;
padding: 12px 16px;
font-size: 18px;
font-family: monospace;
text-transform: uppercase;
border: 2px solid var(--ion-color-medium);
border-radius: 8px;
background: var(--ion-background-color);
color: var(--ion-text-color);
}
.color-hex-input:focus {
outline: none;
border-color: var(--ion-color-primary);
}
.palette-action {
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background: var(--ion-color-primary);
border-radius: 50%;
transition: transform 0.2s ease, background-color 0.2s ease;
color: var(--ion-color-primary-contrast);
}
.palette-action:hover {
transform: scale(1.1);
background: var(--ion-color-primary-shade);
}
.palette-action-danger {
background: var(--ion-color-danger);
}
.palette-action-danger:hover {
background: var(--ion-color-danger-shade);
}
/* Section Content Styles */
.section-heading {
margin: 0 0 16px 0;
color: var(--ion-text-color);
font-size: 24px;
font-weight: 700;
text-align: center;
}
.job-name {
margin: 0 0 16px 0;
color: var(--ion-text-color);
font-size: 22px;
font-weight: 600;
}
.description {
color: var(--ion-text-color);
font-size: 18px;
line-height: 1.6;
}
/* Theme Editor Styles */
.theme-editor-content {
display: flex;
flex-direction: column;
gap: 24px;
padding: 8px;
}
.color-fields-grid {
display: flex;
flex-direction: column;
gap: 12px;
}
.color-field-row {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 12px;
background: var(--ion-color-light);
border-radius: 8px;
}
.color-field-swatch {
width: 40px;
height: 40px;
border-radius: 8px;
border: 2px solid rgba(0, 0, 0, 0.2);
cursor: pointer;
flex-shrink: 0;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.color-field-swatch:hover {
transform: scale(1.1);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.color-field-label {
flex: 1;
font-size: 16px;
font-weight: 600;
color: var(--ion-color-dark);
text-transform: capitalize;
}
.color-field-input {
width: 100px;
padding: 8px 12px;
font-family: monospace;
font-size: 14px;
text-transform: uppercase;
border: 2px solid var(--ion-color-medium);
border-radius: 6px;
background: var(--ion-background-color);
color: var(--ion-text-color);
}
.color-field-input:focus {
outline: none;
border-color: var(--ion-color-primary);
}
.theme-editor-actions {
display: flex;
justify-content: center;
gap: 12px;
flex-wrap: wrap;
}
/* Check Hours Button Styles */
.skip-button {
display: block;
margin: 0 auto 24px;
padding: 12px 32px;
font-size: 16px;
font-weight: 600;
color: var(--ion-color-primary-contrast);
background-color: var(--ion-color-primary);
border: 2px solid var(--ion-color-primary);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.skip-button:hover {
transform: scale(1.05);
background-color: var(--ion-color-primary-shade);
border-color: var(--ion-color-primary-shade);
}
.skip-button:active {
transform: scale(0.95);
}
.button-group {
display: flex;
gap: 20px;
justify-content: center;
margin-bottom: 24px;
}
.option-button {
flex: 1;
max-width: 200px;
padding: 16px 32px;
font-size: 20px;
font-weight: 600;
border: none;
border-radius: 8px;
cursor: pointer;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.option-button:hover {
transform: scale(1.05);
}
.option-button:active {
transform: scale(0.95);
}
.option-button.yes-button {
background-color: var(--ion-color-success);
color: var(--ion-color-success-contrast);
}
.option-button.no-button {
background-color: var(--ion-color-danger);
color: var(--ion-color-danger-contrast);
}
/* Notification Bubble Styles */
.notification-bubbles {
display: flex;
flex-direction: column;
gap: 12px;
max-width: 400px;
margin: 0 auto;
}
.notification-bubble {
background-color: var(--ion-color-warning);
color: var(--ion-color-dark);
padding: 16px 20px;
border-radius: 12px;
font-size: 14px;
font-weight: 600;
line-height: 1.4;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
animation: slideIn 0.3s ease-out;
display: flex;
align-items: center;
gap: 12px;
}
.notification-icon {
font-size: 24px;
flex-shrink: 0;
color: var(--ion-color-dark);
}
.notification-text {
flex: 1;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
/* Logo Upload Section Styles */
.logo-upload-section {
margin-top: 48px;
padding: 32px 24px;
background: var(--ion-background-color-step-50);
border-radius: 12px;
border: 2px solid rgba(var(--ion-color-primary-rgb), 0.2);
}
.section-title-main {
margin: 0 0 32px 0;
color: var(--ion-text-color);
font-size: 32px;
font-weight: 700;
text-align: center;
text-transform: uppercase;
letter-spacing: 1px;
}
.logo-upload-card {
background: var(--ion-background-color);
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.logo-upload-card:last-child {
margin-bottom: 0;
}
.logo-heading {
margin: 0 0 12px 0;
color: var(--ion-text-color);
font-size: 24px;
font-weight: 700;
}
.logo-description {
margin: 0 0 20px 0;
color: var(--ion-color-medium);
font-size: 14px;
line-height: 1.5;
}
.logo-preview-container {
margin-bottom: 20px;
}
.logo-preview {
display: flex;
align-items: center;
justify-content: center;
min-height: 120px;
padding: 20px;
background: var(--ion-background-color-step-100);
border-radius: 8px;
border: 2px solid var(--ion-color-primary);
}
.logo-preview-empty {
border-style: dashed;
border-color: var(--ion-color-medium);
color: var(--ion-color-medium);
font-size: 14px;
font-weight: 600;
}
.logo-image {
max-width: 100%;
height: auto;
display: block;
}
.logo-image-square {
max-height: 80px;
width: auto;
}
.logo-image-full {
max-height: 100px;
max-width: 300px;
}
.logo-actions {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.upload-button {
flex: 1;
min-width: 200px;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 24px;
background: var(--ion-color-primary);
color: var(--ion-color-primary-contrast);
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.upload-button:hover {
background: var(--ion-color-primary-shade);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.upload-button:active {
transform: scale(0.98);
}
.remove-button {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
background: var(--ion-color-danger);
color: var(--ion-color-danger-contrast);
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
}
.remove-button:hover {
background: var(--ion-color-danger-shade);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.remove-button:active {
transform: scale(0.98);
}
.file-input-hidden {
display: none;
}
/* Mobile adjustments */
@media (max-width: 768px) {
.theme-designer-container {
padding-right: 20px;
padding-bottom: 100px; /* Make room for horizontal palette bar */
}
.palette-bar {
top: auto;
bottom: 20px;
right: 20px;
left: 20px;
flex-direction: column;
overflow-x: auto;
padding: 12px;
}
.color-labels {
position: relative;
left: auto;
top: auto;
flex-direction: row;
gap: 8px;
margin-bottom: 8px;
}
.color-label {
height: auto;
padding: 4px 8px;
font-size: 10px;
border-radius: 4px;
box-shadow: none;
}
.color-circles {
flex-direction: row;
gap: 8px;
}
.color-circle,
.palette-action {
width: 36px;
height: 36px;
}
.logo-upload-section {
padding: 24px 16px;
}
.section-title-main {
font-size: 24px;
}
.logo-heading {
font-size: 20px;
}
.logo-actions {
flex-direction: column;
}
.upload-button {
width: 100%;
min-width: unset;
}
}
</style>