Add BrandColorLight for subtle card tinting in menu builder
New BrandColorLight column on Businesses table. Menu builder cards (categories, subcategories, items) get a very subtle tint from the light brand color. White when no color is set. Brand color picker now has both dark and light fields. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b9755a1e72
commit
3bd7585383
4 changed files with 115 additions and 49 deletions
|
|
@ -44,6 +44,7 @@ try {
|
|||
HeaderImageExtension,
|
||||
TaxRate,
|
||||
BrandColor,
|
||||
BrandColorLight,
|
||||
SessionEnabled,
|
||||
SessionLockMinutes,
|
||||
SessionPaymentStrategy,
|
||||
|
|
@ -150,6 +151,7 @@ try {
|
|||
"TaxRate": taxRate,
|
||||
"TaxRatePercent": taxRate * 100,
|
||||
"BrandColor": len(q.BrandColor) ? (left(q.BrandColor, 1) == chr(35) ? q.BrandColor : chr(35) & q.BrandColor) : "",
|
||||
"BrandColorLight": len(q.BrandColorLight) ? (left(q.BrandColorLight, 1) == chr(35) ? q.BrandColorLight : chr(35) & q.BrandColorLight) : "",
|
||||
"SessionEnabled": isNumeric(q.SessionEnabled) ? q.SessionEnabled : 0,
|
||||
"SessionLockMinutes": isNumeric(q.SessionLockMinutes) ? q.SessionLockMinutes : 30,
|
||||
"SessionPaymentStrategy": len(q.SessionPaymentStrategy) ? q.SessionPaymentStrategy : "A",
|
||||
|
|
|
|||
|
|
@ -29,30 +29,27 @@ if (bizId LTE 0) {
|
|||
apiAbort({ "OK": false, "ERROR": "missing_businessid", "MESSAGE": "BusinessID is required" });
|
||||
}
|
||||
|
||||
// Validate color format
|
||||
brandColor = structKeyExists(data, "BrandColor") && isSimpleValue(data.BrandColor) ? trim(data.BrandColor) : "";
|
||||
|
||||
// Allow empty to clear, or validate hex format
|
||||
if (len(brandColor) GT 0) {
|
||||
// Strip leading # if present
|
||||
if (left(brandColor, 1) == chr(35)) {
|
||||
brandColor = right(brandColor, len(brandColor) - 1);
|
||||
}
|
||||
// Must be exactly 6 hex chars
|
||||
if (len(brandColor) != 6 || !reFind("^[0-9A-Fa-f]{6}$", brandColor)) {
|
||||
// Helper to validate and normalize a hex color
|
||||
function normalizeHex(raw) {
|
||||
var c = isSimpleValue(raw) ? trim(raw) : "";
|
||||
if (!len(c)) return "";
|
||||
if (left(c, 1) == chr(35)) c = right(c, len(c) - 1);
|
||||
if (len(c) != 6 || !reFind("^[0-9A-Fa-f]{6}$", c))
|
||||
apiAbort({ "OK": false, "ERROR": "invalid_color", "MESSAGE": "Color must be a valid 6-digit hex color (e.g. 1B4D3E or ##1B4D3E)" });
|
||||
}
|
||||
// Store uppercase, no # prefix
|
||||
brandColor = uCase(brandColor);
|
||||
return uCase(c);
|
||||
}
|
||||
|
||||
brandColor = normalizeHex(structKeyExists(data, "BrandColor") ? data.BrandColor : "");
|
||||
brandColorLight = normalizeHex(structKeyExists(data, "BrandColorLight") ? data.BrandColorLight : "");
|
||||
|
||||
// Update the database
|
||||
queryTimed("
|
||||
UPDATE Businesses
|
||||
SET BrandColor = :color
|
||||
SET BrandColor = :color, BrandColorLight = :colorLight
|
||||
WHERE ID = :bizId
|
||||
", {
|
||||
color: { value: brandColor, cfsqltype: "cf_sql_varchar" },
|
||||
colorLight: { value: brandColorLight, cfsqltype: "cf_sql_varchar" },
|
||||
bizId: { value: bizId, cfsqltype: "cf_sql_integer" }
|
||||
}, { datasource: "payfrit" });
|
||||
|
||||
|
|
@ -60,7 +57,8 @@ writeOutput(serializeJSON({
|
|||
"OK": true,
|
||||
"ERROR": "",
|
||||
"MESSAGE": "Brand color saved",
|
||||
"BRANDCOLOR": brandColor
|
||||
"BRANDCOLOR": brandColor,
|
||||
"BRANDCOLORLIGHT": brandColorLight
|
||||
}));
|
||||
</cfscript>
|
||||
|
||||
|
|
|
|||
|
|
@ -461,14 +461,18 @@ try {
|
|||
arrayAppend(templateLibrary, templatesById[templateID]);
|
||||
}
|
||||
|
||||
// Get business brand color
|
||||
// Get business brand colors
|
||||
brandColor = "";
|
||||
brandColorLight = "";
|
||||
try {
|
||||
qBrand = queryTimed("
|
||||
SELECT BrandColor AS BusinessBrandColor FROM Businesses WHERE ID = :bizId
|
||||
SELECT BrandColor, BrandColorLight FROM Businesses WHERE ID = :bizId
|
||||
", { bizId: businessID }, { datasource: "payfrit" });
|
||||
if (qBrand.recordCount > 0 && len(trim(qBrand.BusinessBrandColor))) {
|
||||
brandColor = left(qBrand.BusinessBrandColor, 1) == chr(35) ? qBrand.BusinessBrandColor : chr(35) & qBrand.BusinessBrandColor;
|
||||
if (qBrand.recordCount > 0) {
|
||||
if (len(trim(qBrand.BrandColor)))
|
||||
brandColor = left(qBrand.BrandColor, 1) == chr(35) ? qBrand.BrandColor : chr(35) & qBrand.BrandColor;
|
||||
if (len(trim(qBrand.BrandColorLight)))
|
||||
brandColorLight = left(qBrand.BrandColorLight, 1) == chr(35) ? qBrand.BrandColorLight : chr(35) & qBrand.BrandColorLight;
|
||||
}
|
||||
} catch (any e) {
|
||||
// Column may not exist yet, ignore
|
||||
|
|
@ -481,6 +485,7 @@ try {
|
|||
response["DEFAULT_MENU_ID"] = defaultMenuID;
|
||||
response["TEMPLATES"] = templateLibrary;
|
||||
response["BRANDCOLOR"] = brandColor;
|
||||
response["BRANDCOLORLIGHT"] = brandColorLight;
|
||||
response["CATEGORY_COUNT"] = arrayLen(categories);
|
||||
response["TEMPLATE_COUNT"] = arrayLen(templateLibrary);
|
||||
response["MENU_COUNT"] = arrayLen(allMenus);
|
||||
|
|
|
|||
|
|
@ -162,7 +162,7 @@
|
|||
|
||||
/* Category Card */
|
||||
.category-card {
|
||||
background: var(--gray-50);
|
||||
background: var(--brand-tint, var(--gray-50));
|
||||
border-radius: 12px;
|
||||
border: 2px solid var(--gray-200);
|
||||
overflow: hidden;
|
||||
|
|
@ -182,7 +182,7 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
background: var(--brand-tint, #fff);
|
||||
cursor: grab;
|
||||
gap: 12px;
|
||||
}
|
||||
|
|
@ -314,7 +314,7 @@
|
|||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
background: #fff;
|
||||
background: var(--brand-tint, #fff);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--gray-200);
|
||||
cursor: grab;
|
||||
|
|
@ -3622,37 +3622,56 @@
|
|||
// Show brand color picker modal
|
||||
showBrandColorPicker() {
|
||||
const currentColor = this.brandColor || '#1B4D3E';
|
||||
document.getElementById('modalTitle').textContent = 'Brand Color';
|
||||
const currentLight = this.brandColorLight || '';
|
||||
document.getElementById('modalTitle').textContent = 'Brand Colors';
|
||||
document.getElementById('modalBody').innerHTML = `
|
||||
<p style="margin-bottom: 16px; color: var(--text-muted);">
|
||||
This color is used for the category bar gradients in the customer app.
|
||||
</p>
|
||||
<div style="display: flex; align-items: center; gap: 16px; margin-bottom: 16px;">
|
||||
<input type="color" id="brandColorInput" value="${currentColor}"
|
||||
style="width: 60px; height: 40px; border: none; cursor: pointer; border-radius: 4px;">
|
||||
<input type="text" id="brandColorHex" value="${currentColor}"
|
||||
style="width: 100px; padding: 8px; border: 1px solid var(--border); border-radius: 4px; font-family: monospace;"
|
||||
pattern="^#[0-9A-Fa-f]{6}$" maxlength="7">
|
||||
<div id="brandColorPreview" style="flex: 1; height: 40px; border-radius: 4px; background: linear-gradient(to bottom, ${currentColor}44, ${currentColor}00, ${currentColor}66);"></div>
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label style="display: block; font-weight: 600; margin-bottom: 6px; font-size: 13px;">Brand Color (Dark)</label>
|
||||
<p style="margin-bottom: 10px; color: var(--text-muted); font-size: 12px;">
|
||||
Used for category bar gradients in the customer app.
|
||||
</p>
|
||||
<div style="display: flex; align-items: center; gap: 16px; margin-bottom: 12px;">
|
||||
<input type="color" id="brandColorInput" value="${currentColor}"
|
||||
style="width: 60px; height: 40px; border: none; cursor: pointer; border-radius: 4px;">
|
||||
<input type="text" id="brandColorHex" value="${currentColor}"
|
||||
style="width: 100px; padding: 8px; border: 1px solid var(--border); border-radius: 4px; font-family: monospace;"
|
||||
pattern="^#[0-9A-Fa-f]{6}$" maxlength="7">
|
||||
<div id="brandColorPreview" style="flex: 1; height: 40px; border-radius: 4px; background: linear-gradient(to bottom, ${currentColor}44, ${currentColor}00, ${currentColor}66);"></div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; flex-wrap: wrap;">
|
||||
<button type="button" class="color-preset" data-color="#1B4D3E" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #1B4D3E;" title="Forest Green"></button>
|
||||
<button type="button" class="color-preset" data-color="#2C3E50" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #2C3E50;" title="Midnight Blue"></button>
|
||||
<button type="button" class="color-preset" data-color="#8B4513" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #8B4513;" title="Saddle Brown"></button>
|
||||
<button type="button" class="color-preset" data-color="#800020" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #800020;" title="Burgundy"></button>
|
||||
<button type="button" class="color-preset" data-color="#1A1A2E" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #1A1A2E;" title="Dark Navy"></button>
|
||||
<button type="button" class="color-preset" data-color="#4A0E4E" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #4A0E4E;" title="Deep Purple"></button>
|
||||
<button type="button" class="color-preset" data-color="#CC5500" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #CC5500;" title="Burnt Orange"></button>
|
||||
<button type="button" class="color-preset" data-color="#355E3B" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #355E3B;" title="Hunter Green"></button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px;">
|
||||
<button type="button" class="color-preset" data-color="#1B4D3E" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #1B4D3E;" title="Forest Green (Default)"></button>
|
||||
<button type="button" class="color-preset" data-color="#2C3E50" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #2C3E50;" title="Midnight Blue"></button>
|
||||
<button type="button" class="color-preset" data-color="#8B4513" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #8B4513;" title="Saddle Brown"></button>
|
||||
<button type="button" class="color-preset" data-color="#800020" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #800020;" title="Burgundy"></button>
|
||||
<button type="button" class="color-preset" data-color="#1A1A2E" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #1A1A2E;" title="Dark Navy"></button>
|
||||
<button type="button" class="color-preset" data-color="#4A0E4E" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #4A0E4E;" title="Deep Purple"></button>
|
||||
<button type="button" class="color-preset" data-color="#CC5500" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #CC5500;" title="Burnt Orange"></button>
|
||||
<button type="button" class="color-preset" data-color="#355E3B" style="width: 32px; height: 32px; border-radius: 4px; border: 2px solid transparent; cursor: pointer; background: #355E3B;" title="Hunter Green"></button>
|
||||
<div style="border-top: 1px solid var(--border); padding-top: 16px; margin-bottom: 20px;">
|
||||
<label style="display: block; font-weight: 600; margin-bottom: 6px; font-size: 13px;">Brand Color (Light)</label>
|
||||
<p style="margin-bottom: 10px; color: var(--text-muted); font-size: 12px;">
|
||||
Subtle tint for menu builder cards and backgrounds. Leave empty for white.
|
||||
</p>
|
||||
<div style="display: flex; align-items: center; gap: 16px;">
|
||||
<input type="color" id="brandColorLightInput" value="${currentLight || '#F5F5F5'}"
|
||||
style="width: 60px; height: 40px; border: none; cursor: pointer; border-radius: 4px;">
|
||||
<input type="text" id="brandColorLightHex" value="${currentLight}"
|
||||
style="width: 100px; padding: 8px; border: 1px solid var(--border); border-radius: 4px; font-family: monospace;"
|
||||
placeholder="#F5F5F5" maxlength="7">
|
||||
<div id="brandColorLightPreview" style="flex: 1; height: 40px; border-radius: 4px; border: 1px solid var(--border); background: ${currentLight || '#fff'};"></div>
|
||||
<button type="button" class="btn btn-secondary" style="font-size: 12px; padding: 6px 10px;" onclick="document.getElementById('brandColorLightHex').value='';document.getElementById('brandColorLightPreview').style.background='#fff';">Clear</button>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display: flex; gap: 8px; justify-content: flex-end;">
|
||||
<button type="button" class="btn btn-secondary" onclick="MenuBuilder.closeModal()">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" onclick="MenuBuilder.saveBrandColor()">Save Color</button>
|
||||
<button type="button" class="btn btn-primary" onclick="MenuBuilder.saveBrandColor()">Save Colors</button>
|
||||
</div>
|
||||
`;
|
||||
this.showModal();
|
||||
|
||||
// Wire up color picker sync
|
||||
// Wire up dark color picker sync
|
||||
const colorInput = document.getElementById('brandColorInput');
|
||||
const hexInput = document.getElementById('brandColorHex');
|
||||
const preview = document.getElementById('brandColorPreview');
|
||||
|
|
@ -3675,7 +3694,7 @@
|
|||
}
|
||||
});
|
||||
|
||||
// Preset buttons
|
||||
// Preset buttons (dark only)
|
||||
document.querySelectorAll('.color-preset').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const color = btn.dataset.color;
|
||||
|
|
@ -3684,13 +3703,37 @@
|
|||
updatePreview(color);
|
||||
});
|
||||
});
|
||||
|
||||
// Wire up light color picker sync
|
||||
const lightInput = document.getElementById('brandColorLightInput');
|
||||
const lightHex = document.getElementById('brandColorLightHex');
|
||||
const lightPreview = document.getElementById('brandColorLightPreview');
|
||||
|
||||
lightInput.addEventListener('input', (e) => {
|
||||
lightHex.value = e.target.value.toUpperCase();
|
||||
lightPreview.style.background = e.target.value;
|
||||
});
|
||||
|
||||
lightHex.addEventListener('input', (e) => {
|
||||
let val = e.target.value;
|
||||
if (!val.startsWith('#')) val = '#' + val;
|
||||
if (/^#[0-9A-Fa-f]{6}$/.test(val)) {
|
||||
lightInput.value = val;
|
||||
lightPreview.style.background = val;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Save brand color
|
||||
async saveBrandColor() {
|
||||
const color = document.getElementById('brandColorHex').value.toUpperCase();
|
||||
if (!/^#[0-9A-Fa-f]{6}$/.test(color)) {
|
||||
this.toast('Invalid color format. Use #RRGGBB', 'error');
|
||||
this.toast('Invalid dark color format. Use #RRGGBB', 'error');
|
||||
return;
|
||||
}
|
||||
const lightColor = (document.getElementById('brandColorLightHex')?.value || '').toUpperCase();
|
||||
if (lightColor && !/^#[0-9A-Fa-f]{6}$/.test(lightColor)) {
|
||||
this.toast('Invalid light color format. Use #RRGGBB or leave empty', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -3698,12 +3741,18 @@
|
|||
const response = await fetch(`${this.config.apiBaseUrl}/businesses/saveBrandColor.cfm`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ BusinessID: this.config.businessId, BrandColor: color })
|
||||
body: JSON.stringify({
|
||||
BusinessID: this.config.businessId,
|
||||
BrandColor: color,
|
||||
BrandColorLight: lightColor
|
||||
})
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.OK) {
|
||||
this.brandColor = color;
|
||||
this.brandColorLight = data.BRANDCOLORLIGHT ? '#' + data.BRANDCOLORLIGHT : '';
|
||||
document.getElementById('brandColorSwatch').style.background = color;
|
||||
this.applyBrandTint();
|
||||
this.closeModal();
|
||||
this.toast('Brand color saved!', 'success');
|
||||
} else {
|
||||
|
|
@ -3715,6 +3764,16 @@
|
|||
}
|
||||
},
|
||||
|
||||
applyBrandTint() {
|
||||
const canvas = document.getElementById('menuCanvas');
|
||||
if (!canvas) return;
|
||||
if (this.brandColorLight && this.brandColorLight.length >= 4) {
|
||||
canvas.style.setProperty('--brand-tint', this.brandColorLight + '0A');
|
||||
} else {
|
||||
canvas.style.setProperty('--brand-tint', 'transparent');
|
||||
}
|
||||
},
|
||||
|
||||
// Load menu from API
|
||||
async loadMenu(menuId = null) {
|
||||
try {
|
||||
|
|
@ -3749,12 +3808,14 @@
|
|||
|
||||
// Store templates from API (default to empty array if not provided)
|
||||
this.templates = data.TEMPLATES || [];
|
||||
// Load brand color if set
|
||||
// Load brand colors
|
||||
if (data.BRANDCOLOR && data.BRANDCOLOR.length > 0) {
|
||||
this.brandColor = data.BRANDCOLOR;
|
||||
const swatch = document.getElementById('brandColorSwatch');
|
||||
if (swatch) swatch.style.background = data.BRANDCOLOR;
|
||||
}
|
||||
this.brandColorLight = data.BRANDCOLORLIGHT || '';
|
||||
this.applyBrandTint();
|
||||
this.renderTemplateLibrary();
|
||||
this.render();
|
||||
} else {
|
||||
|
|
|
|||
Reference in a new issue