diff --git a/frontend/documentation/themes/UsingThemeStore.md b/frontend/documentation/themes/UsingThemeStore.md new file mode 100644 index 0000000..6a3993f --- /dev/null +++ b/frontend/documentation/themes/UsingThemeStore.md @@ -0,0 +1,84 @@ +# Using the Theme Store + +This guide shows how to read and react to the company theme in Vue components. + +## Imports +```js +import { useThemeStore } from "@/stores/theme"; +``` + +## Reading theme tokens +```js +const themeStore = useThemeStore(); +// Access current theme object +console.log(themeStore.currentTheme.primary); +``` + +Theme tokens exposed as CSS variables (runtime-applied to `:root`): +- `--theme-primary`: main brand color +- `--theme-primary-strong`: deeper shade of primary for borders/active states +- `--theme-secondary`: secondary accent color +- `--theme-accent`: softer accent/highlight +- `--theme-gradient-start` / `--theme-gradient-end`: primary gradient stops +- `--theme-secondary-gradient-start` / `--theme-secondary-gradient-end`: secondary gradient stops +- `--theme-surface`: default card/background surface +- `--theme-surface-alt`: alternate surface +- `--theme-border`: standard border color +- `--theme-hover`: hover surface color +- `--theme-text`: primary text color +- `--theme-text-muted`: muted/secondary text +- `--theme-text-dark`: dark text color (use on light surfaces) +- `--theme-text-light`: light text color (use on dark/gradient surfaces) +- Backward-compat (mapped to the above): `--primary-color`, `--primary-600`, `--surface-card`, `--surface-border`, `--surface-hover`, `--text-color` + +## Applying theme in components +### Option 1: Use CSS vars in ` +``` + +### Option 2: Use reactive values in script +```vue + + + +``` + +## Reacting to company changes +The app already applies themes on company change. If you need side-effects in a component: +```js +watch(() => themeStore.currentTheme, (t) => { + console.log("theme changed", t.primary); +}); +``` + +## Adding a new company theme +1) Open `src/stores/theme.js` and add a new entry to `themeMap` with all keys: `primary`, `primaryStrong`, `secondary`, `accent`, `primaryGradientStart/End`, `secondaryGradientStart/End`, `surface`, `surfaceAlt`, `border`, `hover`, `text`, `textMuted`. +2) No further changes are needed; selecting that company will apply the theme. + +## Quick reference for gradients +- Primary gradient: `--theme-gradient-start` → `--theme-gradient-end` +- Secondary gradient: `--theme-secondary-gradient-start` → `--theme-secondary-gradient-end` + +Use primary for main brand moments; secondary for supporting UI accents. diff --git a/frontend/src/App.vue b/frontend/src/App.vue index a85a3c2..afbda38 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,7 +1,10 @@ @@ -34,7 +47,10 @@ onMounted(() => { }" >
- +
@@ -63,11 +79,19 @@ onMounted(() => { flex-direction: row; border-radius: 10px; padding: 10px; - border: 4px solid rgb(235, 230, 230); + border: 4px solid var(--theme-border); max-width: 2500px; width: 100%; margin: 10px auto; height: 90vh; + background: linear-gradient(145deg, var(--theme-surface) 0%, var(--theme-surface-alt) 60%); +} + +.sidebar-column { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 170px; } #display-content { diff --git a/frontend/src/components/CompanySelector.vue b/frontend/src/components/CompanySelector.vue new file mode 100644 index 0000000..a9511e2 --- /dev/null +++ b/frontend/src/components/CompanySelector.vue @@ -0,0 +1,92 @@ + + + + + diff --git a/frontend/src/components/SideBar.vue b/frontend/src/components/SideBar.vue index c60d906..c18f739 100644 --- a/frontend/src/components/SideBar.vue +++ b/frontend/src/components/SideBar.vue @@ -204,15 +204,15 @@ const handleCategoryClick = (category) => { diff --git a/frontend/src/components/calendar/jobs/JobsCalendar.vue b/frontend/src/components/calendar/jobs/JobsCalendar.vue index 4a8abf6..9426645 100644 --- a/frontend/src/components/calendar/jobs/JobsCalendar.vue +++ b/frontend/src/components/calendar/jobs/JobsCalendar.vue @@ -816,7 +816,7 @@ onMounted(async () => { align-items: center; margin-bottom: 20px; padding-bottom: 15px; - border-bottom: 1px solid #e0e0e0; + border-bottom: 1px solid var(--surface-border); } .header-controls { @@ -828,7 +828,7 @@ onMounted(async () => { .day-display-btn { font-weight: 600; font-size: 1.1em; - color: #1976d2 !important; + color: var(--theme-primary) !important; text-transform: none; letter-spacing: normal; min-width: 320px; @@ -867,16 +867,16 @@ onMounted(async () => { .calendar-header-row { display: grid; - border-bottom: 2px solid #e0e0e0; - background-color: #f8f9fa; + border-bottom: 2px solid var(--surface-border); + background-color: var(--surface-hover); } .time-column-header { padding: 16px 8px; font-weight: 600; text-align: center; - border-right: 1px solid #e0e0e0; - color: #666; + border-right: 1px solid var(--surface-border); + color: var(--theme-text-muted); } .foreman-header { @@ -888,19 +888,19 @@ onMounted(async () => { } .foreman-header:hover { - background-color: #f0f0f0; + background-color: var(--surface-hover); } .foreman-name { font-weight: 600; font-size: 0.9em; margin-bottom: 4px; - color: #1976d2; + color: var(--theme-primary); } .foreman-jobs { font-size: 0.75em; - color: #666; + color: var(--theme-text-muted); font-weight: 500; } @@ -912,30 +912,30 @@ onMounted(async () => { .time-row { display: grid; min-height: 40px; - border-bottom: 1px solid #f0f0f0; + border-bottom: 1px solid var(--surface-border); } .time-row:nth-child(odd) { - background-color: #fafafa; + background-color: var(--surface-hover); } .time-column { padding: 8px; - border-right: 1px solid #e0e0e0; + border-right: 1px solid var(--surface-border); display: flex; align-items: center; justify-content: center; - background-color: #f8f9fa; + background-color: var(--surface-hover); } .time-label { font-size: 0.75em; - color: #666; + color: var(--theme-text-muted); font-weight: 500; } .foreman-column { - border-right: 1px solid #e0e0e0; + border-right: 1px solid var(--surface-border); position: relative; cursor: pointer; transition: background-color 0.2s; @@ -943,11 +943,11 @@ onMounted(async () => { } .foreman-column:hover { - background-color: #f0f8ff; + background-color: var(--theme-surface-alt); } .foreman-column.current-time { - background-color: #fff3e0; + background-color: var(--theme-surface-alt); } .foreman-column.drag-over { diff --git a/frontend/src/components/clientView/AdditionalInfoBar.vue b/frontend/src/components/clientView/AdditionalInfoBar.vue index 653b7ad..6e75006 100644 --- a/frontend/src/components/clientView/AdditionalInfoBar.vue +++ b/frontend/src/components/clientView/AdditionalInfoBar.vue @@ -19,9 +19,9 @@ const props = defineProps({ align-items: center; justify-content: center; padding: 0.25rem 1rem; - background: linear-gradient(135deg, #f0f9f0 0%, #e8f5e8 100%); - color: #2d5a47; - border-bottom: 1px solid #c8e6c9; + background: linear-gradient(135deg, var(--theme-gradient-start) 0%, var(--theme-gradient-end) 100%); + color: #fff; + border-bottom: 1px solid var(--theme-primary-strong); font-size: 0.75rem; font-weight: 500; min-height: 30px; diff --git a/frontend/src/components/clientView/TopBar.vue b/frontend/src/components/clientView/TopBar.vue index fc980c7..906f4e7 100644 --- a/frontend/src/components/clientView/TopBar.vue +++ b/frontend/src/components/clientView/TopBar.vue @@ -75,9 +75,9 @@ const formatDate = (date) => { align-items: center; justify-content: space-between; padding: 0.5rem 1rem; - background: linear-gradient(135deg, #0d4f3c 0%, #1a5f4a 100%); + background: linear-gradient(135deg, var(--theme-gradient-start) 0%, var(--theme-gradient-end) 100%); color: #fff; - border-bottom: 1px solid #2d5a47; + border-bottom: 1px solid var(--theme-primary-strong); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); flex-wrap: wrap; gap: 0.5rem; @@ -93,7 +93,7 @@ const formatDate = (date) => { .section-label { font-weight: 500; - color: #adb5bd; + color: var(--theme-text-muted); white-space: nowrap; font-size: 0.85rem; } @@ -130,7 +130,7 @@ const formatDate = (date) => { .customer-name { font-size: 0.75rem; - color: #adb5bd; + color: var(--theme-text-muted); font-weight: 500; } @@ -145,7 +145,7 @@ const formatDate = (date) => { gap: 0.25rem; font-weight: 500; font-size: 0.8rem; - background: #495057; + background: var(--theme-primary-strong); padding: 0.25rem 0.5rem; border-radius: 10px; white-space: nowrap; diff --git a/frontend/src/components/common/DataTable.vue b/frontend/src/components/common/DataTable.vue index 216c7a6..5a9bcd2 100644 --- a/frontend/src/components/common/DataTable.vue +++ b/frontend/src/components/common/DataTable.vue @@ -1190,31 +1190,33 @@ defineExpose({ /* Modern DataTable Styling */ /* Filter Panel Styles */ + .dt-filter-panel { - background: #ffffff; - border: 1px solid #e5e7eb; + background: var(--surface-card); + border: 1px solid var(--surface-border); border-radius: 6px; margin-bottom: 0.5rem; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); } + .dt-filter-header { padding: 0.5rem 0.75rem 0.25rem; - border-bottom: 1px solid #f3f4f6; + border-bottom: 1px solid var(--surface-border); } .dt-filter-title { margin: 0; font-size: 0.75rem; font-weight: 600; - color: #374151; + color: var(--theme-text); display: flex; align-items: center; gap: 0.375rem; } .dt-filter-title i { - color: #6366f1; + color: var(--theme-primary); } .dt-filter-content { @@ -1237,20 +1239,22 @@ defineExpose({ .dt-filter-label { font-size: 0.75rem; font-weight: 500; - color: #374151; + color: var(--theme-text); } + .dt-filter-input { - border: 1px solid #d1d5db; + border: 1px solid var(--surface-border); border-radius: 4px; padding: 0.375rem 0.5rem; font-size: 0.75rem; transition: border-color 0.2s ease; - background: #ffffff; + background: var(--surface-card); } + .dt-filter-input:focus { - border-color: #6366f1; + border-color: var(--theme-primary); outline: none; } @@ -1269,13 +1273,14 @@ defineExpose({ padding: 0.375rem 0.75rem; } + .dt-filter-status { margin-top: 0.5rem; padding: 0.375rem 0.5rem; - background: #f9fafb; + background: var(--surface-hover); border-radius: 4px; font-size: 0.7rem; - color: #6b7280; + color: var(--theme-text-muted); display: flex; align-items: center; gap: 0.375rem; @@ -1283,8 +1288,8 @@ defineExpose({ /* Pagination Panel Styles */ .dt-pagination-panel { - background: #ffffff; - border: 1px solid #e5e7eb; + background: var(--surface-card); + border: 1px solid var(--surface-border); border-radius: 6px; margin-bottom: 0.5rem; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); @@ -1303,12 +1308,12 @@ defineExpose({ display: flex; align-items: center; gap: 0.375rem; - color: #6b7280; + color: var(--theme-text-muted); font-size: 0.75rem; } .dt-pagination-info i { - color: #6366f1; + color: var(--theme-primary); font-size: 0.75rem; } @@ -1320,31 +1325,31 @@ defineExpose({ .dt-pagination-label { font-size: 0.75rem; - color: #374151; + color: var(--theme-text); font-weight: 500; } .dt-pagination-select { - border: 1px solid #d1d5db; + border: 1px solid var(--surface-border); border-radius: 4px; padding: 0.25rem 0.5rem; font-size: 0.75rem; - background: white; + background: var(--surface-card); transition: border-color 0.2s ease; } .dt-pagination-select:focus { - border-color: #6366f1; + border-color: var(--theme-primary); outline: none; } /* Bulk Actions Panel Styles */ .dt-bulk-actions-panel { - background: #fef7ed; - border: 1px solid #fed7aa; + background: var(--theme-surface-alt); + border: 1px solid var(--surface-border); border-radius: 6px; margin-bottom: 0.5rem; - box-shadow: 0 1px 2px rgba(251, 146, 60, 0.1); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); animation: slideInBulk 0.2s ease-out; } @@ -1379,12 +1384,13 @@ defineExpose({ padding: 0.375rem 0.75rem; } + .dt-bulk-actions-status { padding: 0.25rem 0.5rem; - background: rgba(255, 255, 255, 0.8); + background: var(--surface-card); border-radius: 4px; font-size: 0.7rem; - color: #ea580c; + color: var(--theme-secondary); display: flex; align-items: center; gap: 0.375rem; @@ -1392,13 +1398,14 @@ defineExpose({ } /* Global Actions Panel Styles */ + .dt-global-actions-panel { - background: #ffffff; - border: 1px solid #e5e7eb; + background: var(--surface-card); + border: 1px solid var(--surface-border); border-radius: 6px; margin-bottom: 0.5rem; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); - border-left: 3px solid #6366f1; + border-left: 3px solid var(--theme-primary); } .dt-global-actions-content { @@ -1469,12 +1476,13 @@ defineExpose({ color: white; } + .dt-global-actions-status { padding: 0.375rem 0.75rem; - background: #f9fafb; - border-top: 1px solid #f3f4f6; + background: var(--surface-hover); + border-top: 1px solid var(--surface-border); font-size: 0.7rem; - color: #6b7280; + color: var(--theme-text-muted); display: flex; align-items: center; gap: 0.375rem; diff --git a/frontend/src/stores/company.js b/frontend/src/stores/company.js new file mode 100644 index 0000000..88ae656 --- /dev/null +++ b/frontend/src/stores/company.js @@ -0,0 +1,27 @@ +import { defineStore } from "pinia"; + +export const useCompanyStore = defineStore("company", { + state: () => ({ + companies: ["SprinklersNW", "Nuco Yard Care", "Lowe Fencing", "Veritas Stone", "Daniel's Landscape Supplies"], + selectedCompany: "SprinklersNW", + }), + + getters: { + currentCompany: (state) => state.selectedCompany, + }, + + actions: { + setSelectedCompany(companyName) { + if (this.companies.includes(companyName)) { + this.selectedCompany = companyName; + } + }, + + setCompanies(companies = []) { + this.companies = [...companies]; + if (!this.companies.includes(this.selectedCompany)) { + this.selectedCompany = this.companies[0] || null; + } + }, + }, +}); diff --git a/frontend/src/stores/theme.js b/frontend/src/stores/theme.js new file mode 100644 index 0000000..139bb46 --- /dev/null +++ b/frontend/src/stores/theme.js @@ -0,0 +1,131 @@ +import { defineStore } from "pinia"; + +const themeMap = { + SprinklersNW: { + primary: "#0f7ac7", + primaryStrong: "#0c639f", + secondary: "#2ca66f", + accent: "#8fd9a8", + primaryGradientStart: "#0c639f", + primaryGradientEnd: "#1390e0", + secondaryGradientStart: "#2c9b64", + secondaryGradientEnd: "#38c487", + surface: "#cadcf1ff", + surfaceAlt: "#dbfdefff", + border: "#cfe5f7", + hover: "#dcedf9", + text: "#0f172a", + textMuted: "#8798afff", + textDark: "#0b1220", + textLight: "#ffffff", + }, + "Nuco Yard Care": { + primary: "#3b7f2f", + primaryStrong: "#2f6425", + secondary: "#f0c419", + accent: "#f7de6d", + primaryGradientStart: "#2f6425", + primaryGradientEnd: "#4a9f3a", + secondaryGradientStart: "#d2a106", + secondaryGradientEnd: "#f7de6d", + surface: "#f7fbe9", + surfaceAlt: "#f1f4d6", + border: "#dfe8b5", + hover: "#e6efc7", + text: "#1f2a1c", + textMuted: "#4b5b35", + textDark: "#192016", + textLight: "#ffffff", + }, + "Lowe Fencing": { + primary: "#2f3b52", + primaryStrong: "#232d3f", + secondary: "#5fa4ff", + accent: "#9cc6ff", + primaryGradientStart: "#232d3f", + primaryGradientEnd: "#375073", + secondaryGradientStart: "#4f8ee5", + secondaryGradientEnd: "#5fa4ff", + surface: "#f5f7fb", + surfaceAlt: "#e7ecf5", + border: "#ced6e5", + hover: "#d8e1f0", + text: "#0f172a", + textMuted: "#42506a", + textDark: "#0b1220", + textLight: "#ffffff", + }, + "Veritas Stone": { + primary: "#7a6f63", + primaryStrong: "#5e564d", + secondary: "#c3b9ab", + accent: "#d8d0c5", + primaryGradientStart: "#5e564d", + primaryGradientEnd: "#8a8073", + secondaryGradientStart: "#b2a89c", + secondaryGradientEnd: "#cfc6b8", + surface: "#f7f5f2", + surfaceAlt: "#ebe6df", + border: "#d8d0c5", + hover: "#e6dfd7", + text: "#2d2620", + textMuted: "#5a5047", + textDark: "#231c16", + textLight: "#ffffff", + }, + "Daniel's Landscape Supplies": { + primary: "#2f6b2f", + primaryStrong: "#245224", + secondary: "#f28c28", + accent: "#ffc174", + primaryGradientStart: "#245224", + primaryGradientEnd: "#3a8a3a", + secondaryGradientStart: "#f28c28", + secondaryGradientEnd: "#ffc174", + surface: "#f8fbf4", + surfaceAlt: "#f2f1e9", + border: "#d9e5cc", + hover: "#e7f0d8", + text: "#1f2a1f", + textMuted: "#4f5b3f", + textDark: "#162016", + textLight: "#ffffff", + }, +}; + +export const useThemeStore = defineStore("theme", { + state: () => ({ + currentTheme: themeMap.SprinklersNW, + }), + actions: { + applyTheme(companyName) { + const theme = themeMap[companyName] || themeMap.SprinklersNW; + this.currentTheme = theme; + if (typeof document === "undefined") return; + const root = document.documentElement; + root.style.setProperty("--theme-primary", theme.primary); + root.style.setProperty("--theme-primary-strong", theme.primaryStrong); + root.style.setProperty("--theme-secondary", theme.secondary); + root.style.setProperty("--theme-accent", theme.accent); + root.style.setProperty("--theme-gradient-start", theme.primaryGradientStart); + root.style.setProperty("--theme-gradient-end", theme.primaryGradientEnd); + root.style.setProperty("--theme-secondary-gradient-start", theme.secondaryGradientStart); + root.style.setProperty("--theme-secondary-gradient-end", theme.secondaryGradientEnd); + root.style.setProperty("--theme-surface", theme.surface); + root.style.setProperty("--theme-surface-alt", theme.surfaceAlt); + root.style.setProperty("--theme-border", theme.border); + root.style.setProperty("--theme-hover", theme.hover); + root.style.setProperty("--theme-text", theme.text); + root.style.setProperty("--theme-text-muted", theme.textMuted); + root.style.setProperty("--theme-text-dark", theme.textDark); + root.style.setProperty("--theme-text-light", theme.textLight); + // Backwards-compatible overrides for existing CSS variables + root.style.setProperty("--primary-color", theme.primary); + root.style.setProperty("--primary-600", theme.primaryStrong); + root.style.setProperty("--surface-card", theme.surface); + root.style.setProperty("--surface-border", theme.border); + root.style.setProperty("--surface-hover", theme.hover); + root.style.setProperty("--text-color", theme.text); + }, + }, +}); diff --git a/frontend/src/style.css b/frontend/src/style.css index 5cafc24..7f31200 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -1,6 +1,31 @@ :root { + /* Theme tokens with sensible defaults (will be overwritten by runtime theme switcher) */ + --theme-primary: #0f7ac7; + --theme-primary-strong: #0c639f; + --theme-secondary: #2ca66f; + --theme-accent: #8fd9a8; + --theme-gradient-start: #0c639f; + --theme-gradient-end: #1390e0; + --theme-secondary-gradient-start: #2c9b64; + --theme-secondary-gradient-end: #38c487; + --theme-surface: #f4f9ff; + --theme-surface-alt: #e8f4ef; + --theme-border: #cfe5f7; + --theme-hover: #dcedf9; + --theme-text: #0f172a; + --theme-text-muted: #475569; + --theme-text-dark: #0b1220; + --theme-text-light: #ffffff; + + /* Backwards-compatible vars used across components */ + --primary-color: var(--theme-primary); + --primary-600: var(--theme-primary-strong); + --surface-card: var(--theme-surface); + --surface-border: var(--theme-border); + --surface-hover: var(--theme-hover); + --text-color: var(--theme-text); --primary-text-color: #ffffff; - --secondary-background-color: #669084; + --secondary-background-color: var(--theme-secondary); } /* Fix PrimeVue overlay z-index conflicts with Vuetify modals */