feat: Reorganize settings sidebar and add plan-based feature locking

- Add locked state to Plugins sidebar item with plan feature check
- Create Branding section in settings with Appearance, Email Templates, Custom Domains
- Split Domains page into Booking (URLs, redirects) and Custom Domains (BYOD, purchase)
- Add booking_return_url field to Tenant model for customer redirects
- Update SidebarItem component to support locked prop with lock icon
- Move Email Templates from main sidebar to Settings > Branding
- Add communication credits hooks and payment form updates
- Add timezone fields migration and various UI improvements

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-03 01:35:59 -05:00
parent ef58e9fc94
commit 5cef01ad0d
25 changed files with 2220 additions and 330 deletions

View File

@@ -0,0 +1,122 @@
/**
* Color utility functions for generating brand color palettes
*/
/**
* Convert hex color to HSL values
*/
export function hexToHSL(hex: string): { h: number; s: number; l: number } {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
if (!result) return { h: 0, s: 0, l: 0 };
const r = parseInt(result[1], 16) / 255;
const g = parseInt(result[2], 16) / 255;
const b = parseInt(result[3], 16) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
let h = 0;
let s = 0;
const l = (max + min) / 2;
if (max !== min) {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
break;
case g:
h = ((b - r) / d + 2) / 6;
break;
case b:
h = ((r - g) / d + 4) / 6;
break;
}
}
return { h: h * 360, s: s * 100, l: l * 100 };
}
/**
* Convert HSL values to hex color
*/
export function hslToHex(h: number, s: number, l: number): string {
s /= 100;
l /= 100;
const c = (1 - Math.abs(2 * l - 1)) * s;
const x = c * (1 - Math.abs((h / 60) % 2 - 1));
const m = l - c / 2;
let r = 0, g = 0, b = 0;
if (h < 60) { r = c; g = x; b = 0; }
else if (h < 120) { r = x; g = c; b = 0; }
else if (h < 180) { r = 0; g = c; b = x; }
else if (h < 240) { r = 0; g = x; b = c; }
else if (h < 300) { r = x; g = 0; b = c; }
else { r = c; g = 0; b = x; }
const toHex = (n: number) => Math.round((n + m) * 255).toString(16).padStart(2, '0');
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
/**
* Generate a color palette from a base color
*/
export function generateColorPalette(baseColor: string): Record<string, string> {
const { h, s } = hexToHSL(baseColor);
return {
50: hslToHex(h, Math.min(s, 30), 97),
100: hslToHex(h, Math.min(s, 40), 94),
200: hslToHex(h, Math.min(s, 50), 86),
300: hslToHex(h, Math.min(s, 60), 74),
400: hslToHex(h, Math.min(s, 70), 60),
500: hslToHex(h, s, 50),
600: baseColor, // Use the exact primary color for 600
700: hslToHex(h, s, 40),
800: hslToHex(h, s, 32),
900: hslToHex(h, s, 24),
};
}
/**
* Apply a color palette to CSS custom properties
*/
export function applyColorPalette(palette: Record<string, string>): void {
const root = document.documentElement;
Object.entries(palette).forEach(([shade, color]) => {
root.style.setProperty(`--color-brand-${shade}`, color);
});
}
/**
* Apply primary and secondary colors including the secondary color variable
*/
export function applyBrandColors(primaryColor: string, secondaryColor?: string): void {
const palette = generateColorPalette(primaryColor);
applyColorPalette(palette);
// Set the secondary color variable (used for gradients)
const root = document.documentElement;
root.style.setProperty('--color-brand-secondary', secondaryColor || primaryColor);
}
/**
* Default brand color palette (blue)
*/
export const defaultColorPalette: Record<string, string> = {
50: '#eff6ff',
100: '#dbeafe',
200: '#bfdbfe',
300: '#93c5fd',
400: '#60a5fa',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
800: '#1e40af',
900: '#1e3a8a',
};