Add dynamic sidebar text color with brand color contrast

- Add sidebar text color picker to Branding Settings page
- Implement auto-calculated complementary text colors based on brand color luminance
- Dark themes get light tinted text, light themes get dark tinted text
- Add navigation preview showing text on gradient background
- Support 10 new lighter color palettes (Soft Mint, Lavender, Peach, etc.)
- Add CSS utility classes for brand-text with opacity support
- Update sidebar and navigation components to use dynamic text colors
- Add sidebar_text_color field to Tenant model with migration

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-16 22:16:25 -05:00
parent 6a6ad63e7b
commit 725a3c5d84
15 changed files with 365 additions and 54 deletions

View File

@@ -72,7 +72,7 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
return (
<div
className={`flex flex-col h-full text-white shrink-0 transition-all duration-300 ${isCollapsed ? 'w-20' : 'w-64'}`}
className={`flex flex-col h-full text-brand-text shrink-0 transition-all duration-300 ${isCollapsed ? 'w-20' : 'w-64'}`}
style={{
background: `linear-gradient(to bottom right, var(--color-brand-600, ${business.primaryColor}), var(--color-brand-secondary, ${business.secondaryColor || business.primaryColor}))`
}}
@@ -112,7 +112,7 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
{!isCollapsed && business.logoDisplayMode !== 'logo-only' && (
<div className="overflow-hidden">
<h1 className="font-bold leading-tight truncate">{business.name}</h1>
<p className="text-xs text-white/60 truncate">{business.subdomain}.smoothschedule.com</p>
<p className="text-xs text-brand-text/60 truncate">{business.subdomain}.smoothschedule.com</p>
</div>
)}
</>
@@ -331,22 +331,22 @@ const Sidebar: React.FC<SidebarProps> = ({ business, user, isCollapsed, toggleCo
</nav>
{/* User Section */}
<div className="p-4 border-t border-white/10">
<div className="p-4 border-t border-brand-text/10">
<a
href={`${window.location.protocol}//${window.location.host.split('.').slice(-2).join('.')}`}
target="_blank"
rel="noopener noreferrer"
className={`flex items-center gap-2 text-xs text-white/60 mb-3 hover:text-white/80 transition-colors ${isCollapsed ? 'justify-center' : ''}`}
className={`flex items-center gap-2 text-xs text-brand-text/60 mb-3 hover:text-brand-text/80 transition-colors ${isCollapsed ? 'justify-center' : ''}`}
>
<SmoothScheduleLogo className="w-5 h-5 text-white" />
<SmoothScheduleLogo className="w-5 h-5 text-brand-text" />
{!isCollapsed && (
<span className="text-white/60">{t('nav.smoothSchedule')}</span>
<span className="text-brand-text/60">{t('nav.smoothSchedule')}</span>
)}
</a>
<button
onClick={handleSignOut}
disabled={logoutMutation.isPending}
className={`flex items-center gap-3 px-3 py-2 text-sm font-medium text-white/70 hover:text-white hover:bg-white/5 w-full transition-colors rounded-lg ${isCollapsed ? 'justify-center' : ''} disabled:opacity-50`}
className={`flex items-center gap-3 px-3 py-2 text-sm font-medium text-brand-text/70 hover:text-brand-text hover:bg-brand-text/5 w-full transition-colors rounded-lg ${isCollapsed ? 'justify-center' : ''} disabled:opacity-50`}
>
<LogOut size={18} className="shrink-0" />
{!isCollapsed && <span>{t('auth.signOut')}</span>}

View File

@@ -27,12 +27,12 @@ export const SidebarSection: React.FC<SidebarSectionProps> = ({
return (
<div className={`space-y-1 ${className}`}>
{title && !isCollapsed && (
<h3 className="px-4 pt-1 pb-1.5 text-xs font-semibold uppercase tracking-wider text-white/40">
<h3 className="px-4 pt-1 pb-1.5 text-xs font-semibold uppercase tracking-wider text-brand-text/40">
{title}
</h3>
)}
{title && isCollapsed && (
<div className="mx-auto w-8 border-t border-white/20 my-2" />
<div className="mx-auto w-8 border-t border-brand-text/20 my-2" />
)}
{children}
</div>
@@ -83,14 +83,14 @@ export const SidebarItem: React.FC<SidebarItemProps> = ({
? 'text-gray-400 hover:text-gray-500 hover:bg-gray-50 dark:text-gray-500 dark:hover:text-gray-400 dark:hover:bg-gray-800'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-50 dark:text-gray-400 dark:hover:text-white dark:hover:bg-gray-800'
: isActive
? 'bg-white/10 text-white'
? 'bg-brand-text/10 text-brand-text'
: locked
? 'text-white/40 hover:text-white/60 hover:bg-white/5'
: 'text-white/70 hover:text-white hover:bg-white/5';
? 'text-brand-text/40 hover:text-brand-text/60 hover:bg-brand-text/5'
: 'text-brand-text/70 hover:text-brand-text hover:bg-brand-text/5';
const disabledClasses = variant === 'settings'
? 'text-gray-300 dark:text-gray-600 cursor-not-allowed'
: 'text-white/30 cursor-not-allowed';
: 'text-brand-text/30 cursor-not-allowed';
const className = `${baseClasses} ${collapsedClasses} ${disabled ? disabledClasses : colorClasses}`;
@@ -101,7 +101,7 @@ export const SidebarItem: React.FC<SidebarItemProps> = ({
{!isCollapsed && <span className="flex-1">{label}</span>}
{(badge || badgeElement) && !isCollapsed && (
badgeElement || (
<span className="px-2 py-0.5 text-xs rounded-full bg-white/10">{badge}</span>
<span className="px-2 py-0.5 text-xs rounded-full bg-brand-text/10">{badge}</span>
)
)}
</div>
@@ -163,8 +163,8 @@ export const SidebarDropdown: React.FC<SidebarDropdownProps> = ({
isCollapsed ? 'px-3 justify-center' : 'px-4'
} ${
isActive
? 'bg-white/10 text-white'
: 'text-white/70 hover:text-white hover:bg-white/5'
? 'bg-brand-text/10 text-brand-text'
: 'text-brand-text/70 hover:text-brand-text hover:bg-brand-text/5'
}`}
title={label}
>
@@ -180,7 +180,7 @@ export const SidebarDropdown: React.FC<SidebarDropdownProps> = ({
)}
</button>
{isOpen && !isCollapsed && (
<div className="ml-4 mt-1 space-y-0.5 border-l border-white/20 pl-4">
<div className="ml-4 mt-1 space-y-0.5 border-l border-brand-text/20 pl-4">
{children}
</div>
)}
@@ -210,8 +210,8 @@ export const SidebarSubItem: React.FC<SidebarSubItemProps> = ({
to={to}
className={`flex items-center gap-3 py-2 text-sm font-medium rounded-lg transition-colors px-3 ${
isActive
? 'bg-white/10 text-white'
: 'text-white/60 hover:text-white hover:bg-white/5'
? 'bg-brand-text/10 text-brand-text'
: 'text-brand-text/60 hover:text-brand-text hover:bg-brand-text/5'
}`}
title={label}
>
@@ -230,7 +230,7 @@ interface SidebarDividerProps {
*/
export const SidebarDivider: React.FC<SidebarDividerProps> = ({ isCollapsed }) => {
return (
<div className={`my-4 ${isCollapsed ? 'mx-3' : 'mx-4'} border-t border-white/10`} />
<div className={`my-4 ${isCollapsed ? 'mx-3' : 'mx-4'} border-t border-brand-text/10`} />
);
};

View File

@@ -15,7 +15,7 @@ interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
}
const variantClasses: Record<ButtonVariant, string> = {
primary: 'bg-brand-600 hover:bg-brand-700 text-white border-transparent',
primary: 'bg-brand-600 hover:bg-brand-700 text-brand-text border-transparent',
secondary: 'bg-gray-600 hover:bg-gray-700 text-white border-transparent',
outline: 'bg-transparent hover:bg-gray-50 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600',
ghost: 'bg-transparent hover:bg-gray-100 dark:hover:bg-gray-700 text-gray-700 dark:text-gray-300 border-transparent',

View File

@@ -29,8 +29,8 @@ const colorClasses = {
connectorPending: 'bg-gray-200 dark:bg-gray-700',
},
brand: {
active: 'bg-brand-600 text-white',
completed: 'bg-brand-600 text-white',
active: 'bg-brand-600 text-brand-text',
completed: 'bg-brand-600 text-brand-text',
pending: 'bg-gray-200 dark:bg-gray-700 text-gray-500',
textActive: 'text-brand-600 dark:text-brand-400',
textPending: 'text-gray-400',

View File

@@ -46,7 +46,7 @@ const activeColorClasses = {
underline: 'border-green-600 text-green-600 dark:text-green-400',
},
brand: {
active: 'bg-brand-600 text-white',
active: 'bg-brand-600 text-brand-text',
pills: 'bg-brand-100 dark:bg-brand-900/30 text-brand-700 dark:text-brand-300',
underline: 'border-brand-600 text-brand-600 dark:text-brand-400',
},