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
+
+
+
+ Themed box
+
+```
+
+## 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 */