feat: add advanced analytics and territory management system
- Add comprehensive analytics components with export functionality - Implement territory management with manager performance tracking - Add seatmap components for venue layout management - Create customer management features with modal interface - Add advanced hooks for dashboard flags and territory data - Implement seat selection and venue management utilities - Add type definitions for ticketing and seatmap systems 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
482
reactrebuild0825/src/theme/applyBranding.ts
Normal file
482
reactrebuild0825/src/theme/applyBranding.ts
Normal file
@@ -0,0 +1,482 @@
|
||||
/**
|
||||
* Branding Application Utilities
|
||||
*
|
||||
* Comprehensive utilities for applying organization-specific branding
|
||||
* with zero FOUC (Flash of Unstyled Content) support
|
||||
*/
|
||||
|
||||
import type { BrandingTheme, BrandingCache } from '../types/organization';
|
||||
import { DEFAULT_BRANDING_THEME, createThemeChecksum, isValidCssColor } from '../types/organization';
|
||||
|
||||
// CSS custom property mappings for organization branding
|
||||
const CSS_VARIABLE_MAP = {
|
||||
// Core brand colors
|
||||
accent: '--color-accent',
|
||||
bgCanvas: '--color-bg-canvas',
|
||||
bgSurface: '--color-bg-surface',
|
||||
textPrimary: '--color-text-primary',
|
||||
textSecondary: '--color-text-secondary',
|
||||
border: '--color-border-default',
|
||||
ring: '--color-focus-ring',
|
||||
|
||||
// Extended colors (optional)
|
||||
accent600: '--color-accent-600',
|
||||
surfaceRaised: '--color-surface-raised',
|
||||
success: '--color-success',
|
||||
warning: '--color-warning',
|
||||
error: '--color-error',
|
||||
} as const;
|
||||
|
||||
// Additional derived CSS variables that are computed from theme colors
|
||||
const DERIVED_VARIABLES = {
|
||||
accentHover: '--color-accent-hover',
|
||||
accentLight: '--color-accent-light',
|
||||
accentBg: '--color-accent-bg',
|
||||
accentBorder: '--color-accent-border',
|
||||
glassBg: '--color-glass-bg',
|
||||
glassBorder: '--color-glass-border',
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Apply organization branding theme to CSS custom properties
|
||||
* This function updates CSS variables that are consumed by Tailwind utilities
|
||||
*/
|
||||
export function applyBranding(
|
||||
theme: BrandingTheme,
|
||||
targetElement: HTMLElement = document.documentElement
|
||||
): void {
|
||||
try {
|
||||
// Validate theme colors before applying
|
||||
const validation = validateBrandingTheme(theme);
|
||||
if (!validation.valid) {
|
||||
console.warn('Invalid theme colors detected:', validation.errors);
|
||||
// Apply default theme as fallback
|
||||
theme = { ...DEFAULT_BRANDING_THEME };
|
||||
}
|
||||
|
||||
// Apply core theme colors to CSS custom properties
|
||||
Object.entries(CSS_VARIABLE_MAP).forEach(([themeKey, cssVar]) => {
|
||||
const themeValue = theme[themeKey as keyof BrandingTheme];
|
||||
if (themeValue) {
|
||||
targetElement.style.setProperty(cssVar, themeValue);
|
||||
}
|
||||
});
|
||||
|
||||
// Apply derived variables for enhanced theming
|
||||
applyDerivedVariables(theme, targetElement);
|
||||
|
||||
// Store applied theme for reference and cache validation
|
||||
targetElement.setAttribute('data-org-theme', JSON.stringify(theme));
|
||||
targetElement.setAttribute('data-org-theme-checksum', createThemeChecksum(theme));
|
||||
|
||||
// Dispatch custom event for components that need to react to theme changes
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent('org-theme-applied', {
|
||||
detail: { theme }
|
||||
}));
|
||||
}
|
||||
|
||||
console.log('Applied organization branding theme:', theme);
|
||||
} catch (error) {
|
||||
console.error('Failed to apply branding theme:', error);
|
||||
// Apply default theme as emergency fallback
|
||||
applyBranding(DEFAULT_BRANDING_THEME, targetElement);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply derived CSS variables computed from the theme
|
||||
*/
|
||||
function applyDerivedVariables(
|
||||
theme: BrandingTheme,
|
||||
targetElement: HTMLElement
|
||||
): void {
|
||||
// Generate hover variants (slightly darker/lighter)
|
||||
const accentHover = adjustColorBrightness(theme.accent, -0.1);
|
||||
const accentLight = adjustColorBrightness(theme.accent, 0.2);
|
||||
|
||||
// Generate alpha variants for backgrounds and borders
|
||||
const accentBg = addAlphaToColor(theme.accent, 0.1);
|
||||
const accentBorder = addAlphaToColor(theme.accent, 0.3);
|
||||
|
||||
// Generate glass effect colors based on surface color
|
||||
const glassBg = addAlphaToColor(theme.bgSurface, 0.7);
|
||||
const glassBorder = addAlphaToColor(theme.textSecondary, 0.15);
|
||||
|
||||
// Apply derived variables
|
||||
const derivedValues = {
|
||||
[DERIVED_VARIABLES.accentHover]: accentHover,
|
||||
[DERIVED_VARIABLES.accentLight]: accentLight,
|
||||
[DERIVED_VARIABLES.accentBg]: accentBg,
|
||||
[DERIVED_VARIABLES.accentBorder]: accentBorder,
|
||||
[DERIVED_VARIABLES.glassBg]: glassBg,
|
||||
[DERIVED_VARIABLES.glassBorder]: glassBorder,
|
||||
};
|
||||
|
||||
Object.entries(derivedValues).forEach(([cssVar, value]) => {
|
||||
if (value) {
|
||||
targetElement.style.setProperty(cssVar, value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize theme for caching with checksums
|
||||
*/
|
||||
export function serializeTheme(theme: BrandingTheme): string {
|
||||
const serialized = {
|
||||
theme,
|
||||
checksum: createThemeChecksum(theme),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
return JSON.stringify(serialized);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deserialize cached theme with validation
|
||||
*/
|
||||
export function deserializeTheme(serialized: string): BrandingTheme | null {
|
||||
try {
|
||||
const parsed = JSON.parse(serialized);
|
||||
if (parsed.theme && parsed.checksum) {
|
||||
// Validate checksum to ensure theme integrity
|
||||
const currentChecksum = createThemeChecksum(parsed.theme);
|
||||
if (currentChecksum === parsed.checksum) {
|
||||
return parsed.theme;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.warn('Failed to deserialize theme:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update favicon dynamically with error handling
|
||||
*/
|
||||
export function updateFavicon(faviconUrl: string): void {
|
||||
try {
|
||||
// Validate URL format
|
||||
if (!faviconUrl || !isValidUrl(faviconUrl)) {
|
||||
console.warn('Invalid favicon URL:', faviconUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove existing favicon links
|
||||
const existingFavicons = document.querySelectorAll('link[rel*="icon"]');
|
||||
existingFavicons.forEach(link => link.remove());
|
||||
|
||||
// Add new favicon with multiple sizes for better compatibility
|
||||
const sizes = [16, 32, 48, 64];
|
||||
sizes.forEach(size => {
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'icon';
|
||||
link.type = 'image/png';
|
||||
link.sizes = `${size}x${size}`;
|
||||
link.href = faviconUrl;
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
|
||||
// Add fallback ICO favicon
|
||||
const icoLink = document.createElement('link');
|
||||
icoLink.rel = 'shortcut icon';
|
||||
icoLink.type = 'image/x-icon';
|
||||
icoLink.href = faviconUrl;
|
||||
document.head.appendChild(icoLink);
|
||||
|
||||
console.log('Updated favicon:', faviconUrl);
|
||||
} catch (error) {
|
||||
console.warn('Failed to update favicon:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get currently applied organization theme from DOM
|
||||
*/
|
||||
export function getCurrentOrgTheme(
|
||||
targetElement: HTMLElement = document.documentElement
|
||||
): BrandingTheme | null {
|
||||
const themeData = targetElement.getAttribute('data-org-theme');
|
||||
if (!themeData) return null;
|
||||
|
||||
try {
|
||||
const theme = JSON.parse(themeData) as BrandingTheme;
|
||||
// Validate the theme structure
|
||||
const validation = validateBrandingTheme(theme);
|
||||
return validation.valid ? theme : null;
|
||||
} catch (error) {
|
||||
console.warn('Failed to parse current org theme:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current theme differs from applied theme
|
||||
*/
|
||||
export function isThemeApplied(
|
||||
theme: BrandingTheme,
|
||||
targetElement: HTMLElement = document.documentElement
|
||||
): boolean {
|
||||
const appliedTheme = getCurrentOrgTheme(targetElement);
|
||||
if (!appliedTheme) return false;
|
||||
|
||||
const appliedChecksum = targetElement.getAttribute('data-org-theme-checksum');
|
||||
const newChecksum = createThemeChecksum(theme);
|
||||
|
||||
return appliedChecksum === newChecksum;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to default theme values
|
||||
*/
|
||||
export function resetToDefaultTheme(
|
||||
targetElement: HTMLElement = document.documentElement
|
||||
): void {
|
||||
applyBranding(DEFAULT_BRANDING_THEME, targetElement);
|
||||
console.log('Reset to default theme');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate CSS string for a given theme (useful for style injection)
|
||||
*/
|
||||
export function generateThemeCSS(theme: BrandingTheme): string {
|
||||
const cssRules = Object.entries(CSS_VARIABLE_MAP)
|
||||
.map(([themeKey, cssVar]) => {
|
||||
const value = theme[themeKey as keyof BrandingTheme];
|
||||
return value ? ` ${cssVar}: ${value};` : null;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
|
||||
return `:root {\n${cssRules}\n}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate inline style object for React components
|
||||
*/
|
||||
export function generateThemeStyle(theme: BrandingTheme): React.CSSProperties {
|
||||
const style: React.CSSProperties = {};
|
||||
|
||||
Object.entries(CSS_VARIABLE_MAP).forEach(([themeKey, cssVar]) => {
|
||||
const value = theme[themeKey as keyof BrandingTheme];
|
||||
if (value) {
|
||||
(style as any)[cssVar] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate branding theme colors and structure
|
||||
*/
|
||||
export function validateBrandingTheme(theme: Partial<BrandingTheme>): {
|
||||
valid: boolean;
|
||||
errors: string[]
|
||||
} {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Check required properties
|
||||
const requiredProps: Array<keyof BrandingTheme> = [
|
||||
'accent', 'bgCanvas', 'bgSurface', 'textPrimary', 'textSecondary', 'border', 'ring'
|
||||
];
|
||||
|
||||
requiredProps.forEach(prop => {
|
||||
if (!theme[prop]) {
|
||||
errors.push(`Missing required property: ${prop}`);
|
||||
} else if (!isValidCssColor(theme[prop]!)) {
|
||||
errors.push(`Invalid color format for ${prop}: ${theme[prop]}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Check optional properties if present
|
||||
const optionalProps: Array<keyof BrandingTheme> = [
|
||||
'accent600', 'surfaceRaised', 'success', 'warning', 'error'
|
||||
];
|
||||
|
||||
optionalProps.forEach(prop => {
|
||||
if (theme[prop] && !isValidCssColor(theme[prop]!)) {
|
||||
errors.push(`Invalid color format for ${prop}: ${theme[prop]}`);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate contrast ratio between two colors for accessibility validation
|
||||
*/
|
||||
export function calculateContrastRatio(color1: string, color2: string): number {
|
||||
const getLuminance = (color: string): number => {
|
||||
// Remove # and convert to RGB
|
||||
const hex = color.replace('#', '');
|
||||
const r = parseInt(hex.substr(0, 2), 16) / 255;
|
||||
const g = parseInt(hex.substr(2, 2), 16) / 255;
|
||||
const b = parseInt(hex.substr(4, 2), 16) / 255;
|
||||
|
||||
// Apply gamma correction
|
||||
const gamma = (component: number) =>
|
||||
component <= 0.03928 ? component / 12.92 : Math.pow((component + 0.055) / 1.055, 2.4);
|
||||
|
||||
const rLin = gamma(r);
|
||||
const gLin = gamma(g);
|
||||
const bLin = gamma(b);
|
||||
|
||||
// Calculate relative luminance
|
||||
return 0.2126 * rLin + 0.7152 * gLin + 0.0722 * bLin;
|
||||
};
|
||||
|
||||
try {
|
||||
const lum1 = getLuminance(color1);
|
||||
const lum2 = getLuminance(color2);
|
||||
|
||||
const brightest = Math.max(lum1, lum2);
|
||||
const darkest = Math.min(lum1, lum2);
|
||||
|
||||
return (brightest + 0.05) / (darkest + 0.05);
|
||||
} catch (error) {
|
||||
console.warn('Failed to calculate contrast ratio:', error);
|
||||
return 1; // Return minimum contrast on error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate theme accessibility (WCAG AA compliance)
|
||||
*/
|
||||
export function validateThemeAccessibility(theme: BrandingTheme): {
|
||||
valid: boolean;
|
||||
warnings: string[]
|
||||
} {
|
||||
const warnings: string[] = [];
|
||||
|
||||
try {
|
||||
// Check contrast ratios (WCAG AA requires 4.5:1 for normal text, 3:1 for large text)
|
||||
const textCanvasContrast = calculateContrastRatio(theme.textPrimary, theme.bgCanvas);
|
||||
if (textCanvasContrast < 4.5) {
|
||||
warnings.push(`Low contrast between primary text and canvas: ${textCanvasContrast.toFixed(2)}:1 (minimum 4.5:1)`);
|
||||
}
|
||||
|
||||
const textSurfaceContrast = calculateContrastRatio(theme.textPrimary, theme.bgSurface);
|
||||
if (textSurfaceContrast < 4.5) {
|
||||
warnings.push(`Low contrast between primary text and surface: ${textSurfaceContrast.toFixed(2)}:1 (minimum 4.5:1)`);
|
||||
}
|
||||
|
||||
const secondaryTextCanvasContrast = calculateContrastRatio(theme.textSecondary, theme.bgCanvas);
|
||||
if (secondaryTextCanvasContrast < 4.5) {
|
||||
warnings.push(`Low contrast between secondary text and canvas: ${secondaryTextCanvasContrast.toFixed(2)}:1 (minimum 4.5:1)`);
|
||||
}
|
||||
|
||||
const accentCanvasContrast = calculateContrastRatio(theme.accent, theme.bgCanvas);
|
||||
if (accentCanvasContrast < 3) {
|
||||
warnings.push(`Low contrast between accent and canvas: ${accentCanvasContrast.toFixed(2)}:1 (minimum 3:1)`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to validate theme accessibility:', error);
|
||||
warnings.push('Unable to validate accessibility due to color parsing error');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: warnings.length === 0,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
|
||||
/**
|
||||
* Adjust color brightness by a percentage (-1 to 1)
|
||||
*/
|
||||
function adjustColorBrightness(color: string, adjustment: number): string {
|
||||
try {
|
||||
const hex = color.replace('#', '');
|
||||
const r = Math.max(0, Math.min(255, parseInt(hex.substr(0, 2), 16) + Math.round(255 * adjustment)));
|
||||
const g = Math.max(0, Math.min(255, parseInt(hex.substr(2, 2), 16) + Math.round(255 * adjustment)));
|
||||
const b = Math.max(0, Math.min(255, parseInt(hex.substr(4, 2), 16) + Math.round(255 * adjustment)));
|
||||
|
||||
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
||||
} catch (error) {
|
||||
console.warn('Failed to adjust color brightness:', error);
|
||||
return color; // Return original color on error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add alpha channel to a hex color
|
||||
*/
|
||||
function addAlphaToColor(color: string, alpha: number): string {
|
||||
try {
|
||||
const hex = color.replace('#', '');
|
||||
const r = parseInt(hex.substr(0, 2), 16);
|
||||
const g = parseInt(hex.substr(2, 2), 16);
|
||||
const b = parseInt(hex.substr(4, 2), 16);
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
} catch (error) {
|
||||
console.warn('Failed to add alpha to color:', error);
|
||||
return color; // Return original color on error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic URL validation
|
||||
*/
|
||||
function isValidUrl(string: string): boolean {
|
||||
try {
|
||||
new URL(string);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply cached branding immediately (for FOUC prevention)
|
||||
* This function is designed to run before React hydration
|
||||
*/
|
||||
export function applyCachedBranding(): boolean {
|
||||
try {
|
||||
const cached = localStorage.getItem('bct_branding');
|
||||
if (!cached) return false;
|
||||
|
||||
const brandingCache: BrandingCache = JSON.parse(cached);
|
||||
|
||||
// Validate cache timestamp (expire after 24 hours)
|
||||
const cacheAge = Date.now() - brandingCache.timestamp;
|
||||
const maxCacheAge = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
if (cacheAge > maxCacheAge) {
|
||||
console.log('Branding cache expired, will fetch fresh data');
|
||||
localStorage.removeItem('bct_branding');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Apply cached theme immediately
|
||||
applyBranding(brandingCache.theme);
|
||||
|
||||
// Apply cached assets
|
||||
if (brandingCache.faviconUrl) {
|
||||
updateFavicon(brandingCache.faviconUrl);
|
||||
}
|
||||
|
||||
if (brandingCache.logoUrl) {
|
||||
document.documentElement.setAttribute('data-org-logo', brandingCache.logoUrl);
|
||||
}
|
||||
|
||||
console.log('Applied cached branding for organization:', brandingCache.orgId);
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Failed to apply cached branding:', error);
|
||||
// Clear corrupted cache
|
||||
try {
|
||||
localStorage.removeItem('bct_branding');
|
||||
} catch {
|
||||
// Ignore localStorage errors
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
89
reactrebuild0825/src/theme/cssVariables.ts
Normal file
89
reactrebuild0825/src/theme/cssVariables.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* CSS Variables Generator
|
||||
* Converts TypeScript tokens to CSS custom properties
|
||||
*/
|
||||
|
||||
import { lightTokens, darkTokens, type ThemeTokens } from './tokens';
|
||||
|
||||
// Flatten nested token object to CSS variable name-value pairs
|
||||
function flattenTokens(tokens: ThemeTokens, prefix = 'color'): Record<string, string> {
|
||||
const flattened: Record<string, string> = {};
|
||||
|
||||
// Background colors
|
||||
Object.entries(tokens.background).forEach(([key, value]) => {
|
||||
flattened[`--${prefix}-background-${key}`] = value;
|
||||
});
|
||||
|
||||
// Text colors
|
||||
Object.entries(tokens.text).forEach(([key, value]) => {
|
||||
flattened[`--${prefix}-text-${key}`] = value;
|
||||
});
|
||||
|
||||
// Glass colors
|
||||
Object.entries(tokens.glass).forEach(([key, value]) => {
|
||||
flattened[`--${prefix}-glass-${key}`] = value;
|
||||
});
|
||||
|
||||
// Accent colors (nested structure)
|
||||
Object.entries(tokens.accent).forEach(([accentType, accentValues]) => {
|
||||
Object.entries(accentValues).forEach(([key, value]) => {
|
||||
flattened[`--${prefix}-accent-${accentType}-${key}`] = value;
|
||||
});
|
||||
});
|
||||
|
||||
// Semantic colors (nested structure)
|
||||
Object.entries(tokens.semantic).forEach(([semanticType, semanticValues]) => {
|
||||
Object.entries(semanticValues).forEach(([key, value]) => {
|
||||
flattened[`--${prefix}-semantic-${semanticType}-${key}`] = value;
|
||||
});
|
||||
});
|
||||
|
||||
// Border colors
|
||||
Object.entries(tokens.border).forEach(([key, value]) => {
|
||||
flattened[`--${prefix}-border-${key}`] = value;
|
||||
});
|
||||
|
||||
// Focus colors
|
||||
Object.entries(tokens.focus).forEach(([key, value]) => {
|
||||
flattened[`--${prefix}-focus-${key}`] = value;
|
||||
});
|
||||
|
||||
return flattened;
|
||||
}
|
||||
|
||||
// Convert flattened tokens to CSS string
|
||||
function generateCSSRules(variables: Record<string, string>, selector = ':root'): string {
|
||||
const rules = Object.entries(variables)
|
||||
.map(([name, value]) => ` ${name}: ${value};`)
|
||||
.join('\n');
|
||||
|
||||
return `${selector} {\n${rules}\n}`;
|
||||
}
|
||||
|
||||
// Generate complete CSS with both light and dark themes
|
||||
export function generateThemeCSS(): string {
|
||||
const lightVariables = flattenTokens(lightTokens);
|
||||
const darkVariables = flattenTokens(darkTokens);
|
||||
|
||||
const lightCSS = generateCSSRules(lightVariables, ':root');
|
||||
const darkCSS = generateCSSRules(darkVariables, '.dark, [data-theme="dark"]');
|
||||
|
||||
return `/* Auto-generated theme CSS variables */\n/* DO NOT EDIT - Generated from src/theme/tokens.ts */\n\n${lightCSS}\n\n${darkCSS}`;
|
||||
}
|
||||
|
||||
// Get CSS variable name for a token path
|
||||
export function getCSSVariableName(path: string): string {
|
||||
return `var(--color-${path.replace(/\./g, '-')})`;
|
||||
}
|
||||
|
||||
// Apply theme variables to document (for runtime theme switching)
|
||||
export function applyThemeVariables(theme: 'light' | 'dark'): void {
|
||||
if (typeof document === 'undefined') {return;}
|
||||
|
||||
const tokens = theme === 'light' ? lightTokens : darkTokens;
|
||||
const variables = flattenTokens(tokens);
|
||||
|
||||
Object.entries(variables).forEach(([name, value]) => {
|
||||
document.documentElement.style.setProperty(name, value);
|
||||
});
|
||||
}
|
||||
243
reactrebuild0825/src/theme/orgBootstrap.ts
Normal file
243
reactrebuild0825/src/theme/orgBootstrap.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* Organization Bootstrap Script
|
||||
*
|
||||
* This script runs before React mounts to:
|
||||
* 1. Resolve organization from current host
|
||||
* 2. Apply organization theme to prevent FOUC
|
||||
* 3. Set up organization data for the app
|
||||
*/
|
||||
|
||||
import { applyOrgTheme, DEFAULT_ORG_THEME } from './orgTheme';
|
||||
|
||||
import type { DomainResolutionResponse, Organization } from '../stores/organizationStore';
|
||||
|
||||
// Configuration
|
||||
const FUNCTIONS_BASE_URL = import.meta.env.DEV
|
||||
? 'http://localhost:5001/bct-whitelabel-0825/us-central1'
|
||||
: 'https://us-central1-bct-whitelabel-0825.cloudfunctions.net';
|
||||
|
||||
// Bootstrap state
|
||||
let bootstrapPromise: Promise<Organization | null> | null = null;
|
||||
let resolvedOrg: Organization | null = null;
|
||||
|
||||
/**
|
||||
* Bootstrap organization resolution and theming
|
||||
* This function should be called as early as possible
|
||||
*/
|
||||
export async function bootstrapOrganization(): Promise<Organization | null> {
|
||||
// Return cached promise if already running
|
||||
if (bootstrapPromise) {
|
||||
return bootstrapPromise;
|
||||
}
|
||||
|
||||
bootstrapPromise = performBootstrap();
|
||||
return bootstrapPromise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resolved organization (if available)
|
||||
*/
|
||||
export function getBootstrappedOrg(): Organization | null {
|
||||
return resolvedOrg;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if bootstrap has completed
|
||||
*/
|
||||
export function isBootstrapComplete(): boolean {
|
||||
return resolvedOrg !== null || bootstrapPromise !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal bootstrap implementation
|
||||
*/
|
||||
async function performBootstrap(): Promise<Organization | null> {
|
||||
// Get current host outside try block so it's available in catch
|
||||
let host: string = 'unknown-host';
|
||||
|
||||
try {
|
||||
host = window.location.host || 'unknown-host';
|
||||
console.log('Bootstrapping organization for host:', host);
|
||||
|
||||
// Skip resolution for localhost and Firebase hosting - be more aggressive about detection
|
||||
if (host === 'localhost:5173' || host === 'localhost:3000' || host === '127.0.0.1:5173' ||
|
||||
host.includes('firebaseapp.com') || host.includes('web.app') ||
|
||||
host.includes('dev-racer') || host.includes('firebase') ||
|
||||
// Always skip if not in production or if functions aren't available
|
||||
import.meta.env.DEV || !import.meta.env.VITE_FUNCTIONS_AVAILABLE) {
|
||||
console.log('Development/Firebase hosting detected, using default theme for host:', host);
|
||||
applyOrgTheme(DEFAULT_ORG_THEME);
|
||||
|
||||
// Return mock organization for development
|
||||
const mockOrg: Organization = {
|
||||
id: 'dev-org',
|
||||
name: 'Development Organization',
|
||||
slug: 'dev',
|
||||
branding: {
|
||||
theme: DEFAULT_ORG_THEME,
|
||||
},
|
||||
domains: [],
|
||||
};
|
||||
|
||||
resolvedOrg = mockOrg;
|
||||
return mockOrg;
|
||||
}
|
||||
|
||||
// Resolve organization by host with aggressive timeout
|
||||
console.log('Attempting to resolve organization from functions:', FUNCTIONS_BASE_URL);
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.warn('Organization resolution timeout after 2 seconds');
|
||||
controller.abort();
|
||||
}, 2000); // Reduced to 2 second timeout to fail fast
|
||||
|
||||
const response = await fetch(
|
||||
`${FUNCTIONS_BASE_URL}/resolveDomain?host=${encodeURIComponent(host)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
signal: controller.signal,
|
||||
}
|
||||
);
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
console.warn(`No organization found for host: ${host}`);
|
||||
// Apply default theme and continue
|
||||
applyOrgTheme(DEFAULT_ORG_THEME);
|
||||
return null;
|
||||
}
|
||||
|
||||
throw new Error(`Failed to resolve organization: ${response.status}`);
|
||||
}
|
||||
|
||||
const data: DomainResolutionResponse = await response.json();
|
||||
|
||||
// Construct organization object
|
||||
const org: Organization = {
|
||||
id: data.orgId,
|
||||
name: data.name,
|
||||
slug: data.orgId, // Use orgId as fallback slug
|
||||
branding: data.branding,
|
||||
domains: data.domains,
|
||||
};
|
||||
|
||||
// Apply organization theme immediately
|
||||
applyOrgTheme(org.branding.theme);
|
||||
|
||||
// Apply branding (logo, favicon)
|
||||
if (org.branding.logoUrl) {
|
||||
// Store logo URL for header component to use
|
||||
document.documentElement.setAttribute('data-org-logo', org.branding.logoUrl);
|
||||
}
|
||||
|
||||
if (org.branding.faviconUrl) {
|
||||
updateFavicon(org.branding.faviconUrl);
|
||||
}
|
||||
|
||||
// Update page title if organization name is available
|
||||
if (org.name && !document.title.includes(org.name)) {
|
||||
document.title = `${org.name} - Black Canyon Tickets`;
|
||||
}
|
||||
|
||||
console.log('Successfully resolved organization:', org.name);
|
||||
resolvedOrg = org;
|
||||
return org;
|
||||
|
||||
} catch (error) {
|
||||
// Handle specific timeout/abort errors
|
||||
if (error instanceof Error) {
|
||||
if (error.name === 'AbortError') {
|
||||
console.warn('Organization resolution timed out after 2 seconds, using default theme for host:', host);
|
||||
} else if (error.message.includes('fetch') || error.message.includes('network')) {
|
||||
console.warn('Network error during organization resolution:', error.message, 'for host:', host);
|
||||
} else {
|
||||
console.error('Bootstrap failed:', error, 'for host:', host);
|
||||
}
|
||||
} else {
|
||||
console.error('Bootstrap failed with unknown error:', error, 'for host:', host);
|
||||
}
|
||||
|
||||
// Always apply default theme as fallback and create mock org
|
||||
console.log('Applying default theme fallback for host:', host);
|
||||
applyOrgTheme(DEFAULT_ORG_THEME);
|
||||
|
||||
// Return mock org instead of null to ensure app continues
|
||||
const fallbackOrg: Organization = {
|
||||
id: 'fallback-org',
|
||||
name: 'Black Canyon Tickets',
|
||||
slug: 'fallback',
|
||||
branding: {
|
||||
theme: DEFAULT_ORG_THEME,
|
||||
},
|
||||
domains: [],
|
||||
};
|
||||
|
||||
resolvedOrg = fallbackOrg;
|
||||
return fallbackOrg;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update favicon dynamically
|
||||
*/
|
||||
function updateFavicon(faviconUrl: string): void {
|
||||
try {
|
||||
// Remove existing favicon links
|
||||
const existingFavicons = document.querySelectorAll('link[rel*="icon"]');
|
||||
existingFavicons.forEach(link => link.remove());
|
||||
|
||||
// Add new favicon
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'icon';
|
||||
link.type = 'image/x-icon';
|
||||
link.href = faviconUrl;
|
||||
document.head.appendChild(link);
|
||||
|
||||
console.log('Updated favicon:', faviconUrl);
|
||||
} catch (error) {
|
||||
console.warn('Failed to update favicon:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual refresh of organization data
|
||||
* Useful for testing or admin interfaces
|
||||
*/
|
||||
export async function refreshOrganization(): Promise<Organization | null> {
|
||||
// Reset state
|
||||
bootstrapPromise = null;
|
||||
resolvedOrg = null;
|
||||
|
||||
// Remove cached attributes
|
||||
document.documentElement.removeAttribute('data-org-theme');
|
||||
document.documentElement.removeAttribute('data-org-logo');
|
||||
|
||||
// Re-bootstrap
|
||||
return bootstrapOrganization();
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply organization to existing React store
|
||||
* This function bridges the bootstrap data to the Zustand store
|
||||
*/
|
||||
export function applyBootstrapToStore(): void {
|
||||
if (typeof window === 'undefined') {return;}
|
||||
|
||||
// This will be called from React components after store is available
|
||||
const org = getBootstrappedOrg();
|
||||
if (org) {
|
||||
// Import dynamically to avoid circular dependencies
|
||||
import('../stores/organizationStore').then(({ useOrganizationStore }) => {
|
||||
useOrganizationStore.getState().setCurrentOrg(org);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-bootstrap disabled - bootstrap is called explicitly from index.html
|
||||
// This prevents multiple bootstrap calls that can cause conflicts
|
||||
console.log('orgBootstrap.ts loaded - auto-bootstrap disabled (called from HTML)');
|
||||
199
reactrebuild0825/src/theme/orgTheme.ts
Normal file
199
reactrebuild0825/src/theme/orgTheme.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import type { OrgTheme } from '../stores/organizationStore';
|
||||
|
||||
/**
|
||||
* Organization Theme Manager
|
||||
*
|
||||
* Handles applying organization-specific themes to CSS custom properties
|
||||
* This runs before React mounts to prevent FOUC (Flash of Unstyled Content)
|
||||
*/
|
||||
|
||||
// CSS custom property mappings for organization themes
|
||||
const CSS_VARIABLE_MAP = {
|
||||
// Accent colors
|
||||
accent: '--color-accent-500',
|
||||
|
||||
// Background colors
|
||||
bgCanvas: '--color-bg-canvas',
|
||||
bgSurface: '--color-bg-surface',
|
||||
|
||||
// Text colors
|
||||
textPrimary: '--color-text-primary',
|
||||
textSecondary: '--color-text-secondary',
|
||||
} as const;
|
||||
|
||||
// Default fallback theme values
|
||||
export const DEFAULT_ORG_THEME: OrgTheme = {
|
||||
accent: '#F0C457',
|
||||
bgCanvas: '#2B2D2F',
|
||||
bgSurface: '#34373A',
|
||||
textPrimary: '#F1F3F5',
|
||||
textSecondary: '#C9D0D4',
|
||||
};
|
||||
|
||||
/**
|
||||
* Apply organization theme to CSS custom properties
|
||||
* This function updates CSS variables that are consumed by Tailwind utilities
|
||||
*/
|
||||
export function applyOrgTheme(theme: OrgTheme, targetElement: HTMLElement = document.documentElement): void {
|
||||
// Apply theme colors to CSS custom properties
|
||||
Object.entries(CSS_VARIABLE_MAP).forEach(([themeKey, cssVar]) => {
|
||||
const themeValue = theme[themeKey as keyof OrgTheme];
|
||||
if (themeValue) {
|
||||
targetElement.style.setProperty(cssVar, themeValue);
|
||||
}
|
||||
});
|
||||
|
||||
// Note: We apply to the root element only to avoid recursion
|
||||
// Theme variables cascade naturally through CSS custom properties
|
||||
|
||||
// Store applied theme for reference
|
||||
targetElement.setAttribute('data-org-theme', JSON.stringify(theme));
|
||||
|
||||
console.log('Applied organization theme:', theme);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get currently applied organization theme from DOM
|
||||
*/
|
||||
export function getCurrentOrgTheme(targetElement: HTMLElement = document.documentElement): OrgTheme | null {
|
||||
const themeData = targetElement.getAttribute('data-org-theme');
|
||||
if (!themeData) {return null;}
|
||||
|
||||
try {
|
||||
return JSON.parse(themeData) as OrgTheme;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset to default theme values
|
||||
*/
|
||||
export function resetToDefaultTheme(targetElement: HTMLElement = document.documentElement): void {
|
||||
applyOrgTheme(DEFAULT_ORG_THEME, targetElement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if current theme differs from applied theme
|
||||
*/
|
||||
export function isThemeApplied(theme: OrgTheme, targetElement: HTMLElement = document.documentElement): boolean {
|
||||
const appliedTheme = getCurrentOrgTheme(targetElement);
|
||||
if (!appliedTheme) {return false;}
|
||||
|
||||
return Object.keys(theme).every(key => {
|
||||
const themeKey = key as keyof OrgTheme;
|
||||
return theme[themeKey] === appliedTheme[themeKey];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate CSS variable values for a given theme
|
||||
* Useful for preview mode or dynamic style generation
|
||||
*/
|
||||
export function generateThemeCSS(theme: OrgTheme): string {
|
||||
const cssRules = Object.entries(CSS_VARIABLE_MAP)
|
||||
.map(([themeKey, cssVar]) => {
|
||||
const value = theme[themeKey as keyof OrgTheme];
|
||||
return ` ${cssVar}: ${value};`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
return `:root {\n${cssRules}\n}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate inline style object for React components
|
||||
*/
|
||||
export function generateThemeStyle(theme: OrgTheme): React.CSSProperties {
|
||||
const style: React.CSSProperties = {};
|
||||
|
||||
Object.entries(CSS_VARIABLE_MAP).forEach(([themeKey, cssVar]) => {
|
||||
const value = theme[themeKey as keyof OrgTheme];
|
||||
if (value) {
|
||||
(style as any)[cssVar] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate theme color values
|
||||
*/
|
||||
export function validateThemeColors(theme: Partial<OrgTheme>): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Helper to validate hex color
|
||||
const isValidHex = (color: string): boolean => /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(color);
|
||||
|
||||
// Validate each color property
|
||||
Object.entries(theme).forEach(([key, value]) => {
|
||||
if (value && !isValidHex(value)) {
|
||||
errors.push(`Invalid hex color for ${key}: ${value}`);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate contrast ratio between two colors
|
||||
* Used for accessibility validation
|
||||
*/
|
||||
export function calculateContrastRatio(color1: string, color2: string): number {
|
||||
// This is a simplified contrast calculation
|
||||
// In production, you might want to use a more robust library
|
||||
|
||||
const getLuminance = (hex: string): number => {
|
||||
const rgb = parseInt(hex.slice(1), 16);
|
||||
const r = (rgb >> 16) & 0xff;
|
||||
const g = (rgb >> 8) & 0xff;
|
||||
const b = rgb & 0xff;
|
||||
|
||||
const [rs, gs, bs] = [r, g, b].map(c => {
|
||||
c = c / 255;
|
||||
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
|
||||
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
|
||||
};
|
||||
|
||||
const lum1 = getLuminance(color1);
|
||||
const lum2 = getLuminance(color2);
|
||||
|
||||
const brightest = Math.max(lum1, lum2);
|
||||
const darkest = Math.min(lum1, lum2);
|
||||
|
||||
return (brightest + 0.05) / (darkest + 0.05);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate theme accessibility
|
||||
*/
|
||||
export function validateThemeAccessibility(theme: OrgTheme): { valid: boolean; warnings: string[] } {
|
||||
const warnings: string[] = [];
|
||||
|
||||
// Check contrast ratios (WCAG AA requires 4.5:1 for normal text)
|
||||
const textBgContrast = calculateContrastRatio(theme.textPrimary, theme.bgCanvas);
|
||||
if (textBgContrast < 4.5) {
|
||||
warnings.push(`Low contrast between primary text and canvas background: ${textBgContrast.toFixed(2)}:1`);
|
||||
}
|
||||
|
||||
const textSurfaceContrast = calculateContrastRatio(theme.textPrimary, theme.bgSurface);
|
||||
if (textSurfaceContrast < 4.5) {
|
||||
warnings.push(`Low contrast between primary text and surface background: ${textSurfaceContrast.toFixed(2)}:1`);
|
||||
}
|
||||
|
||||
const accentBgContrast = calculateContrastRatio(theme.accent, theme.bgCanvas);
|
||||
if (accentBgContrast < 3) {
|
||||
warnings.push(`Low contrast between accent and canvas background: ${accentBgContrast.toFixed(2)}:1`);
|
||||
}
|
||||
|
||||
return {
|
||||
valid: warnings.length === 0,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
375
reactrebuild0825/src/theme/tokens.ts
Normal file
375
reactrebuild0825/src/theme/tokens.ts
Normal file
@@ -0,0 +1,375 @@
|
||||
/**
|
||||
* Design Tokens - Single Source of Truth
|
||||
* All colors defined here, consumed everywhere else
|
||||
* Change brand colors here and see them propagate throughout the app
|
||||
*/
|
||||
|
||||
// Base color palettes - these are the foundation
|
||||
export const baseColors = {
|
||||
// Brand gold - core identity color
|
||||
gold: {
|
||||
50: '#fefcf0',
|
||||
100: '#fdf7dc',
|
||||
200: '#fbecb8',
|
||||
300: '#f7dc8a',
|
||||
400: '#f2c55a',
|
||||
500: '#d99e34', // Primary gold
|
||||
600: '#c8852d',
|
||||
700: '#a66b26',
|
||||
800: '#855424',
|
||||
900: '#6d4520',
|
||||
},
|
||||
|
||||
// Warm gray - primary neutral palette
|
||||
warmGray: {
|
||||
50: '#f5f4f1',
|
||||
100: '#e8e5df',
|
||||
200: '#d1cbbf',
|
||||
300: '#b0a990',
|
||||
400: '#8a7e6b',
|
||||
500: '#7d7461',
|
||||
600: '#635c51',
|
||||
700: '#4e453d',
|
||||
800: '#39304a',
|
||||
900: '#202030',
|
||||
},
|
||||
|
||||
// Purple - secondary accent
|
||||
purple: {
|
||||
50: '#faf5ff',
|
||||
100: '#f3e8ff',
|
||||
200: '#e9d5ff',
|
||||
300: '#d8b4fe',
|
||||
400: '#c084fc',
|
||||
500: '#a855f7',
|
||||
600: '#9333ea',
|
||||
700: '#7c3aed',
|
||||
800: '#6b21a8',
|
||||
900: '#581c87',
|
||||
},
|
||||
|
||||
// Semantic colors - consistent across themes
|
||||
semantic: {
|
||||
success: '#10b981',
|
||||
warning: '#f59e0b',
|
||||
error: '#ef4444',
|
||||
info: '#3b82f6',
|
||||
},
|
||||
|
||||
// Pure values for special cases
|
||||
pure: {
|
||||
white: '#ffffff',
|
||||
black: '#000000',
|
||||
transparent: 'transparent',
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Theme token structure - semantic naming only
|
||||
export interface ThemeTokens {
|
||||
background: {
|
||||
primary: string;
|
||||
secondary: string;
|
||||
tertiary: string;
|
||||
elevated: string;
|
||||
overlay: string;
|
||||
};
|
||||
brand: {
|
||||
shell: string;
|
||||
shellContrast: string;
|
||||
};
|
||||
text: {
|
||||
primary: string;
|
||||
secondary: string;
|
||||
muted: string;
|
||||
inverse: string;
|
||||
disabled: string;
|
||||
};
|
||||
surface: {
|
||||
subtle: string;
|
||||
card: string;
|
||||
raised: string;
|
||||
};
|
||||
glass: {
|
||||
bg: string;
|
||||
border: string;
|
||||
shadow: string;
|
||||
subtle: string;
|
||||
hover: string;
|
||||
raised: string;
|
||||
};
|
||||
accent: {
|
||||
primary: {
|
||||
50: string;
|
||||
100: string;
|
||||
200: string;
|
||||
300: string;
|
||||
400: string;
|
||||
500: string;
|
||||
600: string;
|
||||
700: string;
|
||||
800: string;
|
||||
900: string;
|
||||
text: string;
|
||||
};
|
||||
secondary: {
|
||||
50: string;
|
||||
100: string;
|
||||
200: string;
|
||||
300: string;
|
||||
400: string;
|
||||
500: string;
|
||||
600: string;
|
||||
700: string;
|
||||
800: string;
|
||||
900: string;
|
||||
text: string;
|
||||
};
|
||||
gold: {
|
||||
50: string;
|
||||
100: string;
|
||||
200: string;
|
||||
300: string;
|
||||
400: string;
|
||||
500: string;
|
||||
600: string;
|
||||
700: string;
|
||||
800: string;
|
||||
900: string;
|
||||
text: string;
|
||||
};
|
||||
};
|
||||
semantic: {
|
||||
success: {
|
||||
bg: string;
|
||||
border: string;
|
||||
text: string;
|
||||
accent: string;
|
||||
};
|
||||
warning: {
|
||||
bg: string;
|
||||
border: string;
|
||||
text: string;
|
||||
accent: string;
|
||||
};
|
||||
error: {
|
||||
bg: string;
|
||||
border: string;
|
||||
text: string;
|
||||
accent: string;
|
||||
};
|
||||
info: {
|
||||
bg: string;
|
||||
border: string;
|
||||
text: string;
|
||||
accent: string;
|
||||
};
|
||||
};
|
||||
border: {
|
||||
default: string;
|
||||
muted: string;
|
||||
strong: string;
|
||||
};
|
||||
shadow: {
|
||||
sm: string;
|
||||
md: string;
|
||||
lg: string;
|
||||
};
|
||||
focus: {
|
||||
ring: string;
|
||||
offset: string;
|
||||
width: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Light theme tokens
|
||||
export const lightTokens: ThemeTokens = {
|
||||
background: {
|
||||
primary: baseColors.pure.white,
|
||||
secondary: '#f8fafc',
|
||||
tertiary: '#f1f5f9',
|
||||
elevated: baseColors.pure.white,
|
||||
overlay: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
brand: {
|
||||
shell: '#f8fafc',
|
||||
shellContrast: baseColors.warmGray[900],
|
||||
},
|
||||
text: {
|
||||
primary: baseColors.warmGray[900],
|
||||
secondary: baseColors.warmGray[800],
|
||||
muted: baseColors.warmGray[600],
|
||||
inverse: baseColors.pure.white,
|
||||
disabled: baseColors.warmGray[500],
|
||||
},
|
||||
surface: {
|
||||
subtle: 'rgba(255, 255, 255, 0.6)',
|
||||
card: 'rgba(255, 255, 255, 0.8)',
|
||||
raised: 'rgba(255, 255, 255, 0.95)',
|
||||
},
|
||||
glass: {
|
||||
bg: 'rgba(255, 255, 255, 0.8)',
|
||||
border: 'rgba(203, 213, 225, 0.3)',
|
||||
shadow: 'rgba(0, 0, 0, 0.1)',
|
||||
subtle: 'rgba(255, 255, 255, 0.6)',
|
||||
hover: 'rgba(255, 255, 255, 0.9)',
|
||||
raised: 'rgba(255, 255, 255, 0.95)',
|
||||
},
|
||||
accent: {
|
||||
primary: {
|
||||
...baseColors.warmGray,
|
||||
text: baseColors.warmGray[800],
|
||||
},
|
||||
secondary: {
|
||||
...baseColors.purple,
|
||||
text: baseColors.purple[600],
|
||||
},
|
||||
gold: {
|
||||
...baseColors.gold,
|
||||
text: baseColors.gold[700], // Darker for better contrast
|
||||
},
|
||||
},
|
||||
semantic: {
|
||||
success: {
|
||||
bg: '#ecfdf5',
|
||||
border: '#bbf7d0',
|
||||
text: '#065f46',
|
||||
accent: baseColors.semantic.success,
|
||||
},
|
||||
warning: {
|
||||
bg: '#fffbeb',
|
||||
border: '#fed7aa',
|
||||
text: '#92400e',
|
||||
accent: baseColors.semantic.warning,
|
||||
},
|
||||
error: {
|
||||
bg: '#fef2f2',
|
||||
border: '#fecaca',
|
||||
text: '#991b1b',
|
||||
accent: baseColors.semantic.error,
|
||||
},
|
||||
info: {
|
||||
bg: '#eff6ff',
|
||||
border: '#bfdbfe',
|
||||
text: '#1e40af',
|
||||
accent: baseColors.semantic.info,
|
||||
},
|
||||
},
|
||||
border: {
|
||||
default: '#e2e8f0',
|
||||
muted: '#f1f5f9',
|
||||
strong: '#cbd5e1',
|
||||
},
|
||||
shadow: {
|
||||
sm: '0 2px 8px rgba(0, 0, 0, 0.08)',
|
||||
md: '0 4px 16px rgba(0, 0, 0, 0.12)',
|
||||
lg: '0 8px 32px rgba(0, 0, 0, 0.16)',
|
||||
},
|
||||
focus: {
|
||||
ring: baseColors.gold[500],
|
||||
offset: baseColors.pure.white,
|
||||
width: '2px',
|
||||
},
|
||||
};
|
||||
|
||||
// Dark theme tokens
|
||||
export const darkTokens: ThemeTokens = {
|
||||
background: {
|
||||
primary: '#2B2D2F', // Charcoal canvas
|
||||
secondary: '#34373A', // Lighter surface
|
||||
tertiary: '#3C4043', // Elevated surface
|
||||
elevated: '#34373A', // Card/panel surfaces
|
||||
overlay: 'rgba(43, 45, 47, 0.8)', // Charcoal overlay
|
||||
},
|
||||
brand: {
|
||||
shell: '#686A6C', // Nardo Grey for header/sidebar
|
||||
shellContrast: '#F2F3F4', // High contrast text on Nardo
|
||||
},
|
||||
text: {
|
||||
primary: '#F1F3F5', // High contrast primary text
|
||||
secondary: '#C9D0D4', // Secondary text
|
||||
muted: '#A6AEB3', // Muted text
|
||||
inverse: '#1B1C1D', // Dark text for filled components
|
||||
disabled: '#7A8084', // Disabled state
|
||||
},
|
||||
surface: {
|
||||
subtle: '#34373A', // Subtle card surfaces
|
||||
card: '#34373A', // Main card surfaces
|
||||
raised: '#3C4043', // Elevated surfaces
|
||||
},
|
||||
glass: {
|
||||
bg: 'rgba(176, 169, 144, 0.1)',
|
||||
border: 'rgba(176, 169, 144, 0.2)',
|
||||
shadow: 'rgba(32, 32, 48, 0.3)',
|
||||
subtle: 'rgba(176, 169, 144, 0.08)',
|
||||
hover: 'rgba(176, 169, 144, 0.15)',
|
||||
raised: 'rgba(176, 169, 144, 0.18)',
|
||||
},
|
||||
accent: {
|
||||
primary: {
|
||||
...baseColors.warmGray,
|
||||
text: baseColors.warmGray[300],
|
||||
},
|
||||
secondary: {
|
||||
...baseColors.purple,
|
||||
text: baseColors.purple[300],
|
||||
},
|
||||
gold: {
|
||||
...baseColors.gold,
|
||||
500: '#F0C457', // Bright gold for dark mode
|
||||
text: '#1B1C1D', // Dark text for filled components
|
||||
},
|
||||
},
|
||||
semantic: {
|
||||
success: {
|
||||
bg: 'rgba(16, 185, 129, 0.1)',
|
||||
border: 'rgba(16, 185, 129, 0.3)',
|
||||
text: '#6ee7b7',
|
||||
accent: baseColors.semantic.success,
|
||||
},
|
||||
warning: {
|
||||
bg: 'rgba(245, 158, 11, 0.1)',
|
||||
border: 'rgba(245, 158, 11, 0.3)',
|
||||
text: '#fcd34d',
|
||||
accent: baseColors.semantic.warning,
|
||||
},
|
||||
error: {
|
||||
bg: 'rgba(239, 68, 68, 0.1)',
|
||||
border: 'rgba(239, 68, 68, 0.3)',
|
||||
text: '#fca5a5',
|
||||
accent: baseColors.semantic.error,
|
||||
},
|
||||
info: {
|
||||
bg: 'rgba(59, 130, 246, 0.1)',
|
||||
border: 'rgba(59, 130, 246, 0.3)',
|
||||
text: '#93c5fd',
|
||||
accent: baseColors.semantic.info,
|
||||
},
|
||||
},
|
||||
border: {
|
||||
default: 'rgba(255, 255, 255, 0.10)', // Subtle white borders
|
||||
muted: 'rgba(255, 255, 255, 0.05)', // Very subtle borders
|
||||
strong: 'rgba(255, 255, 255, 0.15)', // More prominent borders
|
||||
},
|
||||
shadow: {
|
||||
sm: '0 1px 0 rgba(0, 0, 0, 0.35)', // Crisp shadow for cards
|
||||
md: '0 6px 20px rgba(0, 0, 0, 0.35)', // Medium depth
|
||||
lg: '0 8px 32px rgba(0, 0, 0, 0.4)', // Deep shadows
|
||||
},
|
||||
focus: {
|
||||
ring: '#F0C457', // Bright gold for focus
|
||||
offset: '#2B2D2F', // Charcoal background
|
||||
width: '2px',
|
||||
},
|
||||
};
|
||||
|
||||
// Helper function to get tokens for a theme
|
||||
export function getTokens(theme: 'light' | 'dark'): ThemeTokens {
|
||||
return theme === 'light' ? lightTokens : darkTokens;
|
||||
}
|
||||
|
||||
// Type guard for theme values
|
||||
export function isValidTheme(theme: string): theme is 'light' | 'dark' {
|
||||
return theme === 'light' || theme === 'dark';
|
||||
}
|
||||
|
||||
export type Theme = 'light' | 'dark';
|
||||
Reference in New Issue
Block a user