Add i18 support

This commit is contained in:
Urtzi Alfaro
2025-09-22 11:04:03 +02:00
parent ecfc6a1997
commit ee36c45d25
28 changed files with 2307 additions and 565 deletions

View File

@@ -0,0 +1,78 @@
import React from 'react';
import {
languageConfig,
supportedLanguages,
type SupportedLanguage
} from '../../locales';
import { useLanguageSwitcher } from '../../hooks/useLanguageSwitcher';
import { Select } from './Select';
interface LanguageSelectorProps {
className?: string;
variant?: 'compact' | 'full';
}
export function LanguageSelector({
className,
variant = 'full'
}: LanguageSelectorProps) {
const { currentLanguage, changeLanguage, isChanging } = useLanguageSwitcher();
const languageOptions = supportedLanguages.map(lang => ({
value: lang,
label: variant === 'compact'
? `${languageConfig[lang].flag} ${languageConfig[lang].code.toUpperCase()}`
: `${languageConfig[lang].flag} ${languageConfig[lang].nativeName}`,
}));
const handleLanguageChange = (value: string | number | Array<string | number>) => {
if (typeof value === 'string') {
const newLanguage = value as SupportedLanguage;
changeLanguage(newLanguage);
}
};
return (
<Select
value={currentLanguage}
onChange={handleLanguageChange}
options={languageOptions}
className={className}
placeholder="Select language"
disabled={isChanging}
/>
);
}
// Compact version for headers/toolbars
export function CompactLanguageSelector({ className }: { className?: string }) {
return (
<LanguageSelector
variant="compact"
className={className}
/>
);
}
// Hook for language-related utilities
export function useLanguageUtils() {
const { currentLanguage, changeLanguage } = useLanguageSwitcher();
const getCurrentLanguageConfig = () => languageConfig[currentLanguage];
const isRTL = () => getCurrentLanguageConfig().rtl;
const getLanguageFlag = () => getCurrentLanguageConfig().flag;
const getLanguageName = () => getCurrentLanguageConfig().nativeName;
return {
currentLanguage,
languageConfig: getCurrentLanguageConfig(),
isRTL,
getLanguageFlag,
getLanguageName,
changeLanguage,
availableLanguages: supportedLanguages,
};
}

View File

@@ -95,12 +95,15 @@ export const NotificationPanel: React.FC<NotificationPanelProps> = ({
if (!isOpen) return null;
const unreadNotifications = notifications.filter(n => !n.read);
const isMobile = window.innerWidth < 768;
return (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-40 bg-black/20"
className={`fixed inset-0 z-[9998] transition-all duration-300 ${
isMobile ? 'bg-black/50 backdrop-blur-sm' : 'bg-black/20'
}`}
onClick={onClose}
aria-hidden="true"
/>
@@ -108,51 +111,66 @@ export const NotificationPanel: React.FC<NotificationPanelProps> = ({
{/* Panel */}
<div
className={clsx(
"absolute right-0 top-full mt-2 w-96 max-w-sm bg-[var(--bg-primary)] border border-[var(--border-primary)] rounded-lg shadow-xl z-50 max-h-96 flex flex-col",
"fixed z-[9999] bg-[var(--bg-primary)] border border-[var(--border-primary)] shadow-2xl flex flex-col transition-all duration-300 ease-out",
isMobile
? "inset-x-0 bottom-0 rounded-t-2xl max-h-[85vh]"
: "right-0 top-full mt-2 w-96 max-w-sm rounded-xl shadow-xl max-h-96",
className
)}
>
{/* Mobile Handle */}
{isMobile && (
<div className="flex justify-center py-3">
<div className="w-10 h-1 bg-[var(--border-secondary)] rounded-full" />
</div>
)}
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-[var(--border-primary)]">
<div className={`flex items-center justify-between border-b border-[var(--border-primary)] ${
isMobile ? 'px-6 py-4' : 'px-4 py-3'
}`}>
<div className="flex items-center gap-2">
<h3 className="font-semibold text-sm" style={{ color: 'var(--text-primary)' }}>
<h3 className={`font-semibold text-[var(--text-primary)] ${
isMobile ? 'text-lg' : 'text-sm'
}`}>
Notificaciones
</h3>
{unreadNotifications.length > 0 && (
<Badge variant="info" size="sm">
<Badge variant="info" size={isMobile ? "md" : "sm"}>
{unreadNotifications.length} nuevas
</Badge>
)}
</div>
<div className="flex items-center gap-1">
<div className="flex items-center gap-2">
{unreadNotifications.length > 0 && (
<Button
variant="ghost"
size="sm"
size={isMobile ? "md" : "sm"}
onClick={onMarkAllAsRead}
className="h-6 px-2 text-xs"
className={`${isMobile ? 'px-3 py-2 text-sm' : 'h-6 px-2 text-xs'}`}
>
<Check className={`${isMobile ? 'w-4 h-4 mr-2' : 'w-3 h-3 mr-1'}`} />
Marcar todas
</Button>
)}
<Button
variant="ghost"
size="sm"
size={isMobile ? "md" : "sm"}
onClick={onClose}
className="h-6 w-6 p-0"
className={`${isMobile ? 'p-2' : 'h-6 w-6 p-0'} hover:bg-[var(--bg-secondary)] rounded-full`}
>
<X className="h-3 w-3" />
<X className={`${isMobile ? 'w-5 h-5' : 'w-3 h-3'}`} />
</Button>
</div>
</div>
{/* Notifications List */}
<div className="flex-1 overflow-y-auto">
<div className={`flex-1 overflow-y-auto ${isMobile ? 'px-2 py-2' : ''}`}>
{notifications.length === 0 ? (
<div className="p-8 text-center">
<CheckCircle className="w-8 h-8 mx-auto mb-2" style={{ color: 'var(--color-success)' }} />
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
<div className={`text-center ${isMobile ? 'py-12 px-6' : 'p-8'}`}>
<CheckCircle className={`mx-auto mb-3 ${isMobile ? 'w-12 h-12' : 'w-8 h-8'}`} style={{ color: 'var(--color-success)' }} />
<p className={`text-[var(--text-secondary)] ${isMobile ? 'text-base' : 'text-sm'}`}>
No hay notificaciones
</p>
</div>
@@ -165,18 +183,19 @@ export const NotificationPanel: React.FC<NotificationPanelProps> = ({
<div
key={notification.id}
className={clsx(
"p-3 hover:bg-[var(--bg-secondary)] transition-colors",
"transition-colors hover:bg-[var(--bg-secondary)] active:bg-[var(--bg-tertiary)]",
isMobile ? 'px-4 py-4 mx-2 my-1 rounded-lg' : 'p-3',
!notification.read && "bg-[var(--color-info)]/5"
)}
>
<div className="flex gap-3">
<div className={`flex gap-${isMobile ? '4' : '3'}`}>
{/* Icon */}
<div
className="flex-shrink-0 p-1 rounded-full mt-0.5"
className={`flex-shrink-0 rounded-full mt-0.5 ${isMobile ? 'p-2' : 'p-1'}`}
style={{ backgroundColor: getSeverityColor(notification.severity) + '15' }}
>
<SeverityIcon
className="w-3 h-3"
className={`${isMobile ? 'w-5 h-5' : 'w-3 h-3'}`}
style={{ color: getSeverityColor(notification.severity) }}
/>
</div>
@@ -184,50 +203,56 @@ export const NotificationPanel: React.FC<NotificationPanelProps> = ({
{/* Content */}
<div className="flex-1 min-w-0">
{/* Header */}
<div className="flex items-start justify-between gap-2 mb-1">
<div className="flex items-center gap-2">
<Badge variant={getSeverityBadge(notification.severity)} size="sm">
<div className={`flex items-start justify-between gap-2 ${isMobile ? 'mb-2' : 'mb-1'}`}>
<div className={`flex items-center gap-2 ${isMobile ? 'flex-wrap' : ''}`}>
<Badge variant={getSeverityBadge(notification.severity)} size={isMobile ? "md" : "sm"}>
{notification.severity.toUpperCase()}
</Badge>
<Badge variant="secondary" size="sm">
<Badge variant="secondary" size={isMobile ? "md" : "sm"}>
{notification.item_type === 'alert' ? 'Alerta' : 'Recomendación'}
</Badge>
</div>
<span className="text-xs font-medium" style={{ color: 'var(--text-secondary)' }}>
<span className={`font-medium text-[var(--text-secondary)] ${isMobile ? 'text-sm' : 'text-xs'}`}>
{formatTimestamp(notification.timestamp)}
</span>
</div>
{/* Title */}
<p className="text-sm font-medium mb-1 leading-tight" style={{ color: 'var(--text-primary)' }}>
<p className={`font-medium leading-tight text-[var(--text-primary)] ${
isMobile ? 'text-base mb-2' : 'text-sm mb-1'
}`}>
{notification.title}
</p>
{/* Message */}
<p className="text-xs leading-relaxed mb-2" style={{ color: 'var(--text-secondary)' }}>
<p className={`leading-relaxed text-[var(--text-secondary)] ${
isMobile ? 'text-sm mb-4' : 'text-xs mb-2'
}`}>
{notification.message}
</p>
{/* Actions */}
<div className="flex items-center gap-1">
<div className={`flex items-center gap-2 ${isMobile ? 'flex-col sm:flex-row' : ''}`}>
{!notification.read && (
<Button
variant="ghost"
size="sm"
size={isMobile ? "md" : "sm"}
onClick={() => onMarkAsRead(notification.id)}
className="h-6 px-2 text-xs"
className={`${isMobile ? 'w-full sm:w-auto px-4 py-2 text-sm' : 'h-6 px-2 text-xs'}`}
>
<Check className="w-3 h-3 mr-1" />
<Check className={`${isMobile ? 'w-4 h-4 mr-2' : 'w-3 h-3 mr-1'}`} />
Marcar como leído
</Button>
)}
<Button
variant="ghost"
size="sm"
size={isMobile ? "md" : "sm"}
onClick={() => onRemoveNotification(notification.id)}
className="h-6 px-2 text-xs text-red-600 hover:text-red-700"
className={`text-[var(--color-error)] hover:text-[var(--color-error-dark)] ${
isMobile ? 'w-full sm:w-auto px-4 py-2 text-sm' : 'h-6 px-2 text-xs'
}`}
>
<Trash2 className="w-3 h-3 mr-1" />
<Trash2 className={`${isMobile ? 'w-4 h-4 mr-2' : 'w-3 h-3 mr-1'}`} />
Eliminar
</Button>
</div>
@@ -242,13 +267,18 @@ export const NotificationPanel: React.FC<NotificationPanelProps> = ({
{/* Footer */}
{notifications.length > 0 && (
<div className="p-3 border-t border-[var(--border-primary)] bg-[var(--bg-secondary)]">
<div className={`border-t border-[var(--border-primary)] bg-[var(--bg-secondary)] ${
isMobile ? 'px-6 py-4' : 'p-3'
}`}>
<Button
variant="ghost"
size="sm"
size={isMobile ? "md" : "sm"}
onClick={onClearAll}
className="w-full text-xs text-red-600 hover:text-red-700"
className={`w-full text-[var(--color-error)] hover:text-[var(--color-error-dark)] ${
isMobile ? 'px-4 py-3 text-sm' : 'text-xs'
}`}
>
<Trash2 className={`${isMobile ? 'w-4 h-4 mr-2' : 'w-3 h-3 mr-1'}`} />
Limpiar todas las notificaciones
</Button>
</div>

View File

@@ -3,7 +3,7 @@ import { createPortal } from 'react-dom';
import { useNavigate } from 'react-router-dom';
import { useTenant } from '../../stores/tenant.store';
import { useToast } from '../../hooks/ui/useToast';
import { ChevronDown, Building2, Check, AlertCircle, Plus } from 'lucide-react';
import { ChevronDown, Building2, Check, AlertCircle, Plus, X } from 'lucide-react';
interface TenantSwitcherProps {
className?: string;
@@ -92,41 +92,35 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
// Calculate dropdown position
const calculateDropdownPosition = () => {
if (!buttonRef.current) return;
const buttonRect = buttonRef.current.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const isMobile = viewportWidth < 768; // md breakpoint
if (isMobile) {
// On mobile, use full width with margins and position from top
// Check if dropdown would go off bottom of screen
const dropdownHeight = Math.min(400, viewportHeight * 0.7); // Max 70vh
const spaceBelow = viewportHeight - buttonRect.bottom - 8;
const shouldPositionAbove = spaceBelow < dropdownHeight && buttonRect.top > dropdownHeight;
// On mobile, use a modal-style overlay
setDropdownPosition({
top: shouldPositionAbove ? buttonRect.top - dropdownHeight - 8 : buttonRect.bottom + 8,
left: 16, // 1rem margin from screen edge
right: 16, // For full width calculation
width: viewportWidth - 32, // Full width minus margins
top: 0,
left: 0,
width: viewportWidth,
isMobile: true,
});
} else {
// Desktop positioning - align right edge of dropdown with right edge of button
const dropdownWidth = 320; // w-80 (20rem * 16px) - slightly wider for desktop
const dropdownWidth = 360; // Wider for better content display
let left = buttonRect.right - dropdownWidth;
// Ensure dropdown doesn't go off the left edge of the screen
if (left < 16) {
left = 16;
}
// Ensure dropdown doesn't go off the right edge
if (left + dropdownWidth > viewportWidth - 16) {
left = viewportWidth - dropdownWidth - 16;
}
setDropdownPosition({
top: buttonRect.bottom + 8,
left: left,
@@ -175,7 +169,7 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
// Handle creating new tenant
const handleCreateNewTenant = () => {
setIsOpen(false);
navigate('/app/onboarding');
navigate('/app/onboarding?new=true');
};
// Don't render if no tenants available
@@ -200,122 +194,221 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
ref={buttonRef}
onClick={toggleDropdown}
disabled={isLoading}
className="flex items-center space-x-2 px-3 py-2 text-sm font-medium text-text-primary bg-bg-secondary hover:bg-bg-tertiary border border-border-secondary rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-color-primary focus:ring-opacity-20 disabled:opacity-50 disabled:cursor-not-allowed"
className={`
flex items-center justify-between w-full
${showLabel ? 'px-3 py-2.5' : 'px-2.5 py-2.5 justify-center'}
text-sm font-medium text-[var(--text-primary)]
bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)]
border border-[var(--border-secondary)]
rounded-lg transition-all duration-200
focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20
active:scale-[0.98] active:bg-[var(--bg-tertiary)]
disabled:opacity-50 disabled:cursor-not-allowed
min-h-[44px] touch-manipulation
${isOpen ? 'ring-2 ring-[var(--color-primary)]/20 bg-[var(--bg-tertiary)]' : ''}
`}
aria-expanded={isOpen}
aria-haspopup="listbox"
aria-label="Switch tenant"
>
<Building2 className="w-4 h-4 text-text-secondary" />
<div className="flex items-center space-x-2 min-w-0 flex-1">
<div className={`
flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center
${currentTenant ? 'bg-[var(--color-primary)]/10' : 'bg-[var(--bg-tertiary)]'}
`}>
<Building2 className={`w-4 h-4 ${currentTenant ? 'text-[var(--color-primary)]' : 'text-[var(--text-tertiary)]'}`} />
</div>
{showLabel && (
<div className="flex-1 min-w-0 text-left">
<div className="text-sm font-medium text-[var(--text-primary)] truncate">
{currentTenant?.name || 'Select Organization'}
</div>
{currentTenant?.city && (
<div className="text-xs text-[var(--text-secondary)] truncate">
{currentTenant.city}
</div>
)}
</div>
)}
</div>
{showLabel && (
<span className="hidden sm:block max-w-32 truncate">
{currentTenant?.name || 'Select Tenant'}
</span>
<ChevronDown
className={`w-4 h-4 text-[var(--text-secondary)] transition-transform duration-200 flex-shrink-0 ml-2 ${
isOpen ? 'rotate-180' : ''
}`}
/>
)}
<ChevronDown
className={`w-4 h-4 text-text-secondary transition-transform ${
isOpen ? 'rotate-180' : ''
}`}
/>
</button>
{/* Dropdown Menu - Rendered in portal to avoid stacking context issues */}
{isOpen && createPortal(
<div
ref={dropdownRef}
className={`fixed bg-bg-primary border border-border-secondary rounded-lg shadow-lg z-[9999] ${
dropdownPosition.isMobile ? 'mx-4' : ''
}`}
style={{
top: `${dropdownPosition.top}px`,
left: `${dropdownPosition.left}px`,
width: `${dropdownPosition.width}px`,
maxHeight: dropdownPosition.isMobile ? '70vh' : '80vh',
}}
role="listbox"
aria-label="Available tenants"
>
{/* Header */}
<div className={`border-b border-border-primary ${dropdownPosition.isMobile ? 'px-4 py-3' : 'px-3 py-2'}`}>
<h3 className={`font-semibold text-text-primary ${dropdownPosition.isMobile ? 'text-base' : 'text-sm'}`}>
Organizations
</h3>
</div>
{/* Error State */}
{error && (
<div className="px-3 py-2 border-b border-border-primary">
<div className="flex items-center space-x-2 text-color-error text-xs">
<AlertCircle className="w-4 h-4 flex-shrink-0" />
<span>{error}</span>
<button
onClick={handleRetry}
className="ml-auto text-color-primary hover:text-color-primary-dark underline"
>
Retry
</button>
</div>
</div>
<>
{/* Mobile Backdrop */}
{dropdownPosition.isMobile && (
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-[9998]"
onClick={() => setIsOpen(false)}
/>
)}
{/* Tenant List */}
<div className={`overflow-y-auto ${dropdownPosition.isMobile ? 'max-h-[60vh]' : 'max-h-80'}`}>
{availableTenants.map((tenant) => (
<button
key={tenant.id}
onClick={() => handleTenantSwitch(tenant.id)}
disabled={isLoading}
className={`w-full text-left hover:bg-bg-secondary focus:bg-bg-secondary focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed transition-colors ${
dropdownPosition.isMobile ? 'px-4 py-4 active:bg-bg-tertiary' : 'px-3 py-3'
}`}
role="option"
aria-selected={tenant.id === currentTenant?.id}
>
<div className="flex items-center justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
tenant.id === currentTenant?.id
? 'bg-color-primary text-white'
: 'bg-color-primary/10 text-color-primary'
}`}>
<Building2 className="w-4 h-4" />
</div>
<div className="min-w-0 flex-1">
<p className="text-sm font-medium text-text-primary truncate">
{tenant.name}
</p>
<p className="text-xs text-text-secondary truncate">
{tenant.city}
</p>
{/* Dropdown Content */}
<div
ref={dropdownRef}
className={`
fixed z-[9999] bg-[var(--bg-primary)] border border-[var(--border-primary)]
shadow-2xl transition-all duration-300 ease-out
${dropdownPosition.isMobile
? 'inset-x-0 bottom-0 rounded-t-2xl max-h-[85vh]'
: 'rounded-xl shadow-xl'
}
`}
style={
dropdownPosition.isMobile
? {}
: {
top: `${dropdownPosition.top}px`,
left: `${dropdownPosition.left}px`,
width: `${dropdownPosition.width}px`,
maxHeight: '400px',
}
}
role="listbox"
aria-label="Available tenants"
>
{/* Mobile Handle */}
{dropdownPosition.isMobile && (
<div className="flex justify-center py-3">
<div className="w-10 h-1 bg-[var(--border-secondary)] rounded-full" />
</div>
)}
{/* Header */}
<div className={`
flex items-center justify-between
border-b border-[var(--border-primary)]
${dropdownPosition.isMobile ? 'px-6 py-4' : 'px-4 py-3'}
`}>
<h3 className={`font-semibold text-[var(--text-primary)] ${
dropdownPosition.isMobile ? 'text-lg' : 'text-sm'
}`}>
Organizations
</h3>
{dropdownPosition.isMobile && (
<button
onClick={() => setIsOpen(false)}
className="p-2 hover:bg-[var(--bg-secondary)] rounded-full transition-colors"
>
<X className="w-5 h-5 text-[var(--text-secondary)]" />
</button>
)}
</div>
{/* Error State */}
{error && (
<div className={`border-b border-[var(--border-primary)] ${
dropdownPosition.isMobile ? 'px-6 py-4' : 'px-4 py-3'
}`}>
<div className="flex items-center space-x-3 text-[var(--color-error)]">
<AlertCircle className="w-5 h-5 flex-shrink-0" />
<span className="text-sm flex-1">{error}</span>
<button
onClick={handleRetry}
className="text-[var(--color-primary)] hover:text-[var(--color-primary-dark)] underline text-sm font-medium"
>
Retry
</button>
</div>
</div>
)}
{/* Tenant List */}
<div className={`
overflow-y-auto
${dropdownPosition.isMobile ? 'max-h-[50vh] px-2 py-2' : 'max-h-72 py-1'}
`}>
{availableTenants.map((tenant) => (
<button
key={tenant.id}
onClick={() => handleTenantSwitch(tenant.id)}
disabled={isLoading}
className={`
w-full text-left rounded-lg transition-all duration-200
hover:bg-[var(--bg-secondary)] focus:bg-[var(--bg-secondary)]
active:scale-[0.98] active:bg-[var(--bg-tertiary)]
focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20
disabled:opacity-50 disabled:cursor-not-allowed
${dropdownPosition.isMobile ? 'px-4 py-4 mx-2 my-1 min-h-[60px]' : 'px-3 py-3 mx-1 my-0.5'}
${tenant.id === currentTenant?.id ? 'bg-[var(--color-primary)]/5 ring-1 ring-[var(--color-primary)]/20' : ''}
`}
role="option"
aria-selected={tenant.id === currentTenant?.id}
>
<div className="flex items-center justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center space-x-3">
<div className={`
${dropdownPosition.isMobile ? 'w-12 h-12' : 'w-10 h-10'}
rounded-full flex items-center justify-center flex-shrink-0 transition-colors
${tenant.id === currentTenant?.id
? 'bg-[var(--color-primary)] text-white'
: 'bg-[var(--color-primary)]/10 text-[var(--color-primary)]'
}
`}>
<Building2 className={`${dropdownPosition.isMobile ? 'w-6 h-6' : 'w-5 h-5'}`} />
</div>
<div className="min-w-0 flex-1">
<p className={`font-medium text-[var(--text-primary)] truncate ${
dropdownPosition.isMobile ? 'text-base' : 'text-sm'
}`}>
{tenant.name}
</p>
{tenant.city && (
<p className={`text-[var(--text-secondary)] truncate ${
dropdownPosition.isMobile ? 'text-sm' : 'text-xs'
}`}>
{tenant.city}
</p>
)}
</div>
</div>
</div>
{tenant.id === currentTenant?.id && (
<Check className={`text-[var(--color-success)] flex-shrink-0 ml-3 ${
dropdownPosition.isMobile ? 'w-6 h-6' : 'w-5 h-5'
}`} />
)}
</div>
</button>
))}
</div>
{tenant.id === currentTenant?.id && (
<Check className="w-4 h-4 text-color-success flex-shrink-0 ml-2" />
)}
</div>
{/* Footer */}
<div className={`
border-t border-[var(--border-primary)]
${dropdownPosition.isMobile ? 'px-6 py-6' : 'px-4 py-3'}
`}>
<button
onClick={handleCreateNewTenant}
className={`
w-full flex items-center justify-center gap-3
${dropdownPosition.isMobile ? 'px-6 py-4 text-base' : 'px-4 py-2.5 text-sm'}
text-white font-semibold
bg-gradient-to-r from-[var(--color-primary)] to-[var(--color-primary-dark)]
hover:from-[var(--color-primary-dark)] hover:to-[var(--color-primary)]
shadow-md hover:shadow-lg
rounded-lg transition-all duration-200
focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/30
active:scale-[0.98]
border border-[var(--color-primary)]/30
`}
>
<Plus className={`${dropdownPosition.isMobile ? 'w-5 h-5' : 'w-4 h-4'}`} />
<span>Agregar Nueva Organización</span>
</button>
))}
</div>
</div>
{/* Footer */}
<div className={`border-t border-border-primary bg-bg-secondary rounded-b-lg ${
dropdownPosition.isMobile ? 'px-4 py-3' : 'px-3 py-2'
}`}>
<button
onClick={handleCreateNewTenant}
className={`w-full flex items-center justify-center gap-2 ${
dropdownPosition.isMobile ? 'px-4 py-3' : 'px-3 py-2'
} text-color-primary hover:text-color-primary-dark hover:bg-bg-tertiary rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-color-primary focus:ring-opacity-20`}
>
<Plus className="w-4 h-4" />
<span className={`font-medium ${dropdownPosition.isMobile ? 'text-sm' : 'text-xs'}`}>
Add New Organization
</span>
</button>
</div>
</div>,
</>,
document.body
)}