masoudmarandi's picture
inside each and ALL modals, all email, name, etc. entry fields, icons, colours, etc. all must be compatible with rest of landing page, must be universally compatible across all modals, modals css/icons/boxes/entry fields/colours/etc. must be compatible with main landing page
975a49c verified
class CustomModal extends HTMLElement {
connectedCallback() {
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
/*
Custom Color Palette Utilities
To ensure compatibility with the main Tailwind config inside Shadow DOM.
Primary = Teal, Secondary = Indigo
*/
:host {
--color-primary-50: #f0fdfa;
--color-primary-100: #ccfbf1;
--color-primary-200: #99f6e4;
--color-primary-300: #5eead4;
--color-primary-400: #2dd4bf;
--color-primary-500: #14b8a6;
--color-primary-600: #0d9488;
--color-primary-700: #0f766e;
--color-primary-800: #115e59;
--color-primary-900: #134e4a;
--color-secondary-50: #eef2ff;
--color-secondary-100: #e0e7ff;
--color-secondary-500: #6366f1;
--color-secondary-600: #4f46e5;
--color-secondary-900: #312e81;
}
.text-primary-400 { color: var(--color-primary-400); }
.text-primary-500 { color: var(--color-primary-500); }
.text-primary-600 { color: var(--color-primary-600); }
.text-primary-700 { color: var(--color-primary-700); }
.text-primary-800 { color: var(--color-primary-800); }
.bg-primary-50 { background-color: var(--color-primary-50); }
.bg-primary-100 { background-color: var(--color-primary-100); }
.bg-primary-500 { background-color: var(--color-primary-500); }
.bg-primary-600 { background-color: var(--color-primary-600); }
.bg-primary-700 { background-color: var(--color-primary-700); }
.bg-primary-800 { background-color: var(--color-primary-800); }
.border-primary-100 { border-color: var(--color-primary-100); }
.border-primary-200 { border-color: var(--color-primary-200); }
.border-primary-300 { border-color: var(--color-primary-300); }
.border-primary-500 { border-color: var(--color-primary-500); }
.border-primary-600 { border-color: var(--color-primary-600); }
.ring-primary-500 { --tw-ring-color: var(--color-primary-500); }
.ring-primary-400 { --tw-ring-color: var(--color-primary-400); }
.text-secondary-500 { color: var(--color-secondary-500); }
.text-secondary-600 { color: var(--color-secondary-600); }
.bg-secondary-500 { background-color: var(--color-secondary-500); }
.bg-secondary-600 { background-color: var(--color-secondary-600); }
.bg-secondary-900 { background-color: var(--color-secondary-900); }
.border-secondary-500 { border-color: var(--color-secondary-500); }
:host {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(15, 23, 42, 0.85) 0%, rgba(13, 148, 136, 0.15) 100%);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
z-index: 10000;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s ease-out;
padding: 1rem;
}
:host([open]) {
display: flex;
opacity: 1;
}
.modal-wrapper {
position: relative;
width: 100%;
max-width: 680px;
perspective: 1000px;
}
.modal-content {
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
border-radius: 1.5rem;
max-height: 92vh;
display: flex;
flex-direction: column;
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.1),
0 25px 50px -12px rgba(0, 0, 0, 0.25),
0 0 100px -20px rgba(13, 148, 136, 0.15);
transform: scale(0.92) translateY(20px) rotateX(2deg);
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
border: 1px solid rgba(226, 232, 240, 0.8);
overflow: hidden;
}
:host([open]) .modal-content {
transform: scale(1) translateY(0) rotateX(0deg);
}
/* Decorative gradient bar */
.modal-content::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #0d9488 0%, #14b8a6 50%, #5eead4 100%);
}
.modal-header {
padding: 2rem 2rem 1.5rem;
display: flex;
justify-content: space-between;
align-items: flex-start;
border-bottom: 1px solid rgba(226, 232, 240, 0.6);
background: linear-gradient(180deg, rgba(255, 255, 255, 0.8) 0%, rgba(248, 250, 252, 0.5) 100%);
position: relative;
}
.header-content {
flex: 1;
padding-right: 1rem;
}
.header-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.375rem 0.875rem;
background: linear-gradient(135deg, #f0fdfa 0%, #ccfbf1 100%);
border: 1px solid #99f6e4;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 700;
color: #0f766e;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 1rem;
}
.header-badge svg {
width: 14px;
height: 14px;
}
.modal-header h3 {
margin: 0;
font-size: 1.75rem;
font-weight: 800;
background: linear-gradient(135deg, #0f172a 0%, #334155 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1.2;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
letter-spacing: -0.02em;
}
.header-subtitle {
margin-top: 0.5rem;
font-size: 0.95rem;
color: #64748b;
line-height: 1.5;
}
.close-btn {
background: white;
border: 1px solid #e2e8f0;
cursor: pointer;
padding: 0.75rem;
border-radius: 0.75rem;
color: #64748b;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.close-btn:hover {
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
border-color: #fecaca;
color: #dc2626;
transform: rotate(90deg) scale(1.05);
box-shadow: 0 4px 12px rgba(220, 38, 38, 0.15);
}
.close-btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(13, 148, 136, 0.3), 0 0 0 6px rgba(13, 148, 136, 0.1);
}
.close-btn svg {
width: 22px;
height: 22px;
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
}
.modal-body {
padding: 2rem;
color: #334155;
line-height: 1.7;
overflow-y: auto;
font-size: 0.95rem;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
}
.modal-footer {
padding: 1.5rem 2rem;
border-top: 1px solid rgba(226, 232, 240, 0.6);
display: flex;
justify-content: flex-end;
gap: 1rem;
background: linear-gradient(180deg, rgba(248, 250, 252, 0.8) 0%, #ffffff 100%);
}
.btn {
padding: 0.875rem 1.75rem;
border-radius: 0.75rem;
font-size: 0.925rem;
font-weight: 700;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
border: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
letter-spacing: 0.01em;
}
.btn:focus-visible {
outline: none;
box-shadow: 0 0 0 3px rgba(13, 148, 136, 0.3), 0 0 0 6px rgba(13, 148, 136, 0.1);
}
.btn svg {
width: 18px;
height: 18px;
}
.btn-secondary {
background: white;
color: #475569;
border: 1.5px solid #cbd5e1;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.btn-secondary:hover {
background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%);
border-color: #94a3b8;
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.btn-primary {
background: linear-gradient(135deg, #0d9488 0%, #0f766e 100%);
color: white;
box-shadow:
0 4px 14px rgba(13, 148, 136, 0.35),
0 2px 6px rgba(13, 148, 136, 0.2);
position: relative;
overflow: hidden;
}
.btn-primary::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
transition: left 0.5s ease;
}
.btn-primary:hover::before {
left: 100%;
}
.btn-primary:hover {
background: linear-gradient(135deg, #0f766e 0%, #115e59 100%);
transform: translateY(-2px);
box-shadow:
0 6px 20px rgba(13, 148, 136, 0.4),
0 4px 12px rgba(13, 148, 136, 0.25);
}
/* Enhanced scrollbar */
.modal-body::-webkit-scrollbar {
width: 8px;
}
.modal-body::-webkit-scrollbar-track {
background: rgba(241, 245, 249, 0.5);
border-radius: 4px;
}
.modal-body::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, #cbd5e1 0%, #94a3b8 100%);
border-radius: 4px;
border: 2px solid rgba(241, 245, 249, 0.5);
}
.modal-body::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
}
/* Animations */
@keyframes modalPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(13, 148, 136, 0.4); }
50% { box-shadow: 0 0 0 8px rgba(13, 148, 136, 0); }
}
.modal-pulse {
animation: modalPulse 2s ease-in-out infinite;
}
/* Input Fields Consistency */
.modal-body input[type="text"],
.modal-body input[type="email"],
.modal-body input[type="tel"],
.modal-body textarea {
transition: all 0.2s ease;
border-radius: 0.5rem;
border: 1px solid #cbd5e1;
padding: 0.625rem 1rem; /* py-2.5 px-4 equivalent */
font-size: 0.95rem;
width: 100%;
background-color: white;
box-sizing: border-box; /* Ensure padding doesn't expand width */
}
.modal-body input[type="text"]:focus,
.modal-body input[type="email"]:focus,
.modal-body input[type="tel"]:focus,
.modal-body textarea:focus {
outline: none;
border-color: #14b8a6;
box-shadow: 0 0 0 3px rgba(20, 184, 166, 0.1);
}
.modal-body input[type="text"]:hover,
.modal-body input[type="email"]:hover,
.modal-body input[type="tel"]:hover,
.modal-body textarea:hover {
border-color: #94a3b8;
}
.modal-body label {
display: block;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #64748b;
margin-bottom: 0.5rem;
}
</style>
<div class="modal-wrapper">
<div class="modal-content" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div class="modal-header">
<div class="header-content">
<div class="header-badge" id="modal-badge">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>
<span>Get Started</span>
</div>
<h3 id="modal-title">Modal Title</h3>
<p class="header-subtitle" id="modal-subtitle">Subtitle text goes here</p>
</div>
<button class="close-btn" id="close-modal" aria-label="Close modal">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>
</div>
<div class="modal-body" id="modal-body">
Modal content goes here.
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="modal-cancel">Cancel</button>
<button class="btn btn-primary" id="modal-confirm">Confirm</button>
</div>
</div>
</div>
`;
// Event listeners
this.shadowRoot.getElementById('close-modal').addEventListener('click', () => this.close());
this.shadowRoot.getElementById('modal-cancel').addEventListener('click', () => this.close());
this.shadowRoot.querySelector('.modal-content').addEventListener('click', (e) => e.stopPropagation());
this.addEventListener('click', () => this.close());
}
open(title, content, confirmText = 'Confirm', onConfirm = null, triggerElement = null) {
// Store the element that triggered the modal to restore focus later
this.triggerElement = triggerElement || document.activeElement;
this.shadowRoot.getElementById('modal-title').textContent = title;
this.shadowRoot.getElementById('modal-body').innerHTML = content;
const confirmBtn = this.shadowRoot.getElementById('modal-confirm');
confirmBtn.textContent = confirmText;
if (onConfirm) {
confirmBtn.style.display = 'inline-flex';
confirmBtn.onclick = () => {
onConfirm();
this.close();
};
} else {
confirmBtn.style.display = 'none';
}
this.setAttribute('open', '');
document.body.style.overflow = 'hidden';
// Add ESC key listener
this.handleEscape = (e) => {
if (e.key === 'Escape') {
this.close();
}
};
this.addEventListener('keydown', this.handleEscape);
// Focus management: Focus the first interactive element inside the modal
// Use setTimeout to ensure the modal is rendered in the DOM
setTimeout(() => {
const focusableSelectors = 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])';
const modalContent = this.shadowRoot.querySelector('.modal-content');
const focusableElements = Array.from(modalContent.querySelectorAll(focusableSelectors));
// Filter out hidden elements
const visibleFocusableElements = focusableElements.filter(el =>
el.offsetParent !== null && window.getComputedStyle(el).visibility !== 'hidden'
);
if (visibleFocusableElements.length > 0) {
visibleFocusableElements[0].focus();
} else {
// Fallback to close button
this.shadowRoot.getElementById('close-modal').focus();
}
}, 50);
}
close() {
this.removeAttribute('open');
document.body.style.overflow = '';
// Remove ESC key listener
if (this.handleEscape) {
this.removeEventListener('keydown', this.handleEscape);
this.handleEscape = null;
}
// Restore focus to the trigger element
if (this.triggerElement && document.contains(this.triggerElement)) {
this.triggerElement.focus();
}
}
}
customElements.define('custom-modal', CustomModal);