more WCAG compliance changes: contrast and aria added
This commit is contained in:
parent
bc635acd86
commit
584d39db3d
|
|
@ -49,28 +49,37 @@ export default function AboutSection(props: AboutSectionProps = {}) {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="about" className="about-section">
|
<section id="about" className="about-section" aria-labelledby="about-title">
|
||||||
<div className="about-section__container">
|
<div className="about-section__container">
|
||||||
{/* Section Header */}
|
{/* Section Header */}
|
||||||
<div className="about-section__header">
|
<header className="about-section__header">
|
||||||
<h2 className="about-section__title">
|
<h2 id="about-title" className="about-section__title">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="about-section__subtitle">
|
<p className="about-section__subtitle">
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</p>
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Skip link for keyboard navigation */}
|
||||||
|
<div className="about-section__skip">
|
||||||
|
<a href="#features" className="sr-only sr-only-focusable">
|
||||||
|
Skip to features section
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Personal Photo and Bio */}
|
{/* Personal Photo and Bio */}
|
||||||
<div className="about-section__content">
|
<div className="about-section__content" role="main">
|
||||||
<div className="about-section__image-wrapper">
|
<div className="about-section__image-wrapper">
|
||||||
<img
|
<img
|
||||||
src={profileImage}
|
src={profileImage}
|
||||||
alt={`${name} - Web Developer`}
|
alt={`Professional headshot of ${name}, Web Developer and Software Engineer`}
|
||||||
className="about-section__image"
|
className="about-section__image"
|
||||||
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
<div className="about-section__image-overlay" aria-label={`${name}`}></div>
|
<div className="about-section__image-overlay" aria-hidden="true"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="about-section__bio-section">
|
<div className="about-section__bio-section">
|
||||||
|
|
@ -88,28 +97,57 @@ export default function AboutSection(props: AboutSectionProps = {}) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Feature Cards Grid */}
|
{/* Feature Cards Grid */}
|
||||||
<div className="about-section__features">
|
<div
|
||||||
{featureCards.map((card, index) => {
|
id="features"
|
||||||
const IconComponent = card.icon;
|
className="about-section__features"
|
||||||
return (
|
role="region"
|
||||||
<div
|
aria-labelledby="features-heading"
|
||||||
key={index}
|
>
|
||||||
className={`about-section__feature-card about-section__feature-card--${card.colorClass}`}
|
<h3 id="features-heading" className="sr-only">My Areas of Expertise</h3>
|
||||||
>
|
|
||||||
<div className="about-section__feature-header">
|
<div className="about-section__features-grid">
|
||||||
<IconComponent className={`about-section__feature-icon about-section__feature-icon--${card.colorClass}`} />
|
{featureCards.map((card, index) => {
|
||||||
<h3 className={`about-section__feature-title about-section__feature-title--${card.colorClass}`}>
|
const IconComponent = card.icon;
|
||||||
{card.title}
|
return (
|
||||||
</h3>
|
<article
|
||||||
</div>
|
key={index}
|
||||||
<div className="about-section__feature-content">
|
className={`about-section__feature-card about-section__feature-card--${card.colorClass}`}
|
||||||
<p className={`about-section__feature-description about-section__feature-description--${card.colorClass}`}>
|
tabIndex={0}
|
||||||
{card.description}
|
role="group"
|
||||||
</p>
|
aria-labelledby={`feature-${index}-title`}
|
||||||
</div>
|
aria-describedby={`feature-${index}-description`}
|
||||||
</div>
|
>
|
||||||
);
|
<header className="about-section__feature-header">
|
||||||
})}
|
<IconComponent
|
||||||
|
className={`about-section__feature-icon about-section__feature-icon--${card.colorClass}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
role="img"
|
||||||
|
aria-label={`${card.title} icon`}
|
||||||
|
/>
|
||||||
|
<h4
|
||||||
|
id={`feature-${index}-title`}
|
||||||
|
className={`about-section__feature-title about-section__feature-title--${card.colorClass}`}
|
||||||
|
>
|
||||||
|
{card.title}
|
||||||
|
</h4>
|
||||||
|
</header>
|
||||||
|
<div className="about-section__feature-content">
|
||||||
|
<p
|
||||||
|
id={`feature-${index}-description`}
|
||||||
|
className={`about-section__feature-description about-section__feature-description--${card.colorClass}`}
|
||||||
|
>
|
||||||
|
{card.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Status indicator for screen readers */}
|
||||||
|
<div className="sr-only">
|
||||||
|
Expertise area {index + 1} of {featureCards.length}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -48,50 +48,112 @@ export default function CertificationsSection(props: CertificationsSectionProps
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
// Sort certifications by year in descending order (newest first)
|
// Sort certifications by year in descending order (newest first)
|
||||||
const sortedCertifications = [...certifications].sort((a, b) => {
|
const sortedCertifications = [...certifications].sort((a, b) => {
|
||||||
return parseInt(b.year) - parseInt(a.year);
|
return parseInt(b.year) - parseInt(a.year);
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="certifications" className="certifications-section">
|
<section id="certifications" className="certifications-section" aria-labelledby="certifications-title">
|
||||||
<div className="certifications-section__container">
|
<div className="certifications-section__container">
|
||||||
{/* Section Header */}
|
{/* Section Header */}
|
||||||
<div className="certifications-section__header">
|
<header className="certifications-section__header">
|
||||||
<h2 className="certifications-section__title">
|
<h2 id="certifications-title" className="certifications-section__title">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="certifications-section__subtitle">
|
<p className="certifications-section__subtitle">
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</p>
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Skip link for keyboard navigation */}
|
||||||
|
<div className="certifications-section__skip">
|
||||||
|
<a href="#contact" className="sr-only sr-only-focusable">
|
||||||
|
Skip to contact section
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Certifications sorting info for screen readers */}
|
||||||
|
<div className="certifications-section__info sr-only">
|
||||||
|
<p>
|
||||||
|
Certifications are sorted by year, showing the most recent first.
|
||||||
|
Total: {sortedCertifications.length} certifications and qualifications.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Certifications Grid */}
|
{/* Certifications Grid */}
|
||||||
<div className="certifications-section__grid">
|
<div
|
||||||
|
className="certifications-section__grid"
|
||||||
|
role="region"
|
||||||
|
aria-label="Professional certifications and qualifications"
|
||||||
|
>
|
||||||
{sortedCertifications.map((cert, index) => (
|
{sortedCertifications.map((cert, index) => (
|
||||||
<div key={index} className="certifications-section__card">
|
<article
|
||||||
<div className="certifications-section__card-header">
|
key={index}
|
||||||
|
className="certifications-section__card"
|
||||||
|
tabIndex={0}
|
||||||
|
role="group"
|
||||||
|
aria-labelledby={`cert-${index}-title`}
|
||||||
|
aria-describedby={`cert-${index}-details`}
|
||||||
|
>
|
||||||
|
<header className="certifications-section__card-header">
|
||||||
{cert.icon ? (
|
{cert.icon ? (
|
||||||
<img
|
<img
|
||||||
src={cert.icon}
|
src={cert.icon}
|
||||||
alt={cert.issuer}
|
alt={`${cert.issuer} logo`}
|
||||||
className="certifications-section__card-icon-image"
|
className="certifications-section__card-icon-image"
|
||||||
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Award className="certifications-section__card-icon" />
|
<Award
|
||||||
|
className="certifications-section__card-icon"
|
||||||
|
aria-hidden="true"
|
||||||
|
role="img"
|
||||||
|
aria-label="Certification award icon"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<h3 className="certifications-section__card-title">{cert.name}</h3>
|
<h3
|
||||||
<p className="certifications-section__card-issuer">{cert.issuer}</p>
|
id={`cert-${index}-title`}
|
||||||
</div>
|
className="certifications-section__card-title"
|
||||||
|
>
|
||||||
|
{cert.name}
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
className="certifications-section__card-issuer"
|
||||||
|
aria-label={`Issued by ${cert.issuer}`}
|
||||||
|
>
|
||||||
|
{cert.issuer}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
<div className="certifications-section__card-content">
|
<div className="certifications-section__card-content">
|
||||||
<span className="certifications-section__card-badge">
|
<span
|
||||||
|
className="certifications-section__card-badge"
|
||||||
|
aria-label={`Completed in ${cert.year}`}
|
||||||
|
>
|
||||||
{cert.year}
|
{cert.year}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Hidden details for screen readers */}
|
||||||
|
<div id={`cert-${index}-details`} className="sr-only">
|
||||||
|
Certification {index + 1} of {sortedCertifications.length}.
|
||||||
|
{cert.name} from {cert.issuer}, completed in {cert.year}.
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Certifications summary for screen readers */}
|
||||||
|
<div className="certifications-section__summary sr-only" aria-live="polite">
|
||||||
|
<p>
|
||||||
|
Certifications timeline: Spanning from {sortedCertifications[sortedCertifications.length - 1]?.year} to {sortedCertifications[0]?.year},
|
||||||
|
including {sortedCertifications.filter(cert => cert.issuer.includes('Udemy')).length} online courses and
|
||||||
|
{sortedCertifications.filter(cert => cert.issuer.includes('TU Darmstadt')).length} academic degree.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -18,11 +18,9 @@ interface ContactSectionProps {
|
||||||
locationValue?: string;
|
locationValue?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Create contact form with backend access
|
|
||||||
export default function ContactSection(props: ContactSectionProps = {}) {
|
export default function ContactSection(props: ContactSectionProps = {}) {
|
||||||
const texts = getTexts().contact;
|
const texts = getTexts().contact;
|
||||||
|
|
||||||
// Use centralized texts as defaults, allow props to override
|
|
||||||
const {
|
const {
|
||||||
title = texts.title,
|
title = texts.title,
|
||||||
subtitle = texts.subtitle,
|
subtitle = texts.subtitle,
|
||||||
|
|
@ -39,40 +37,81 @@ export default function ContactSection(props: ContactSectionProps = {}) {
|
||||||
locationValue = texts.locationValue,
|
locationValue = texts.locationValue,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
// Create email link with pre-filled subject and body
|
// Enhanced email handler with better accessibility
|
||||||
const handleEmailClick = () => {
|
const handleEmailClick = () => {
|
||||||
const subject = encodeURIComponent("Portfolio Inquiry - Let's discuss my project");
|
const email = getEmailAddress();
|
||||||
|
const subject = encodeURIComponent("Portfolio Inquiry - Let's discuss your project");
|
||||||
const body = encodeURIComponent("Hello Sascha,\n\nI visited your portfolio and would like to discuss a potential project or collaboration.\n\nBest regards,");
|
const body = encodeURIComponent("Hello Sascha,\n\nI visited your portfolio and would like to discuss a potential project or collaboration.\n\nBest regards,");
|
||||||
window.location.href = `mailto:info@sascha-bach.de?subject=${subject}&body=${body}`;
|
|
||||||
|
// Announce action to screen readers
|
||||||
|
const announcement = document.createElement('div');
|
||||||
|
announcement.setAttribute('aria-live', 'polite');
|
||||||
|
announcement.setAttribute('class', 'sr-only');
|
||||||
|
announcement.textContent = 'Opening email client with pre-filled message';
|
||||||
|
document.body.appendChild(announcement);
|
||||||
|
|
||||||
|
window.location.href = `mailto:${email}?subject=${subject}&body=${body}`;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(announcement);
|
||||||
|
}, 1000);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Replace the hardcoded email with obfuscated construction
|
|
||||||
const getEmailAddress = () => {
|
const getEmailAddress = () => {
|
||||||
const parts = ['freelancer', 'sascha-bach', 'de'];
|
const parts = ['freelancer', 'sascha-bach', 'de'];
|
||||||
return parts[0] + '@' + parts[1] + '.' + parts[2];
|
return parts[0] + '@' + parts[1] + '.' + parts[2];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Enhanced social link handler with announcements
|
||||||
|
const handleSocialClick = (platform: string, url: string) => {
|
||||||
|
const announcement = document.createElement('div');
|
||||||
|
announcement.setAttribute('aria-live', 'polite');
|
||||||
|
announcement.setAttribute('class', 'sr-only');
|
||||||
|
announcement.textContent = `Opening ${platform} profile in new tab`;
|
||||||
|
document.body.appendChild(announcement);
|
||||||
|
|
||||||
|
window.open(url, '_blank', 'noopener,noreferrer');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(announcement);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="contact" className="contact-section">
|
<section id="contact" className="contact-section" aria-labelledby="contact-title">
|
||||||
<div className="contact-section__container">
|
<div className="contact-section__container">
|
||||||
{/* Section Header */}
|
{/* Section Header */}
|
||||||
<div className="contact-section__header">
|
<header className="contact-section__header">
|
||||||
<h2 className="contact-section__title">
|
<h2 id="contact-title" className="contact-section__title">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="contact-section__subtitle">
|
<p className="contact-section__subtitle">
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</p>
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Skip link for keyboard navigation */}
|
||||||
|
<div className="contact-section__skip">
|
||||||
|
<a href="#contact-form" className="sr-only sr-only-focusable">
|
||||||
|
Skip to contact form
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contact Content Grid */}
|
{/* Contact Content Grid */}
|
||||||
<div className="contact-section__grid">
|
<div className="contact-section__grid" role="main">
|
||||||
{/* Let's Connect Card */}
|
{/* Let's Connect Card */}
|
||||||
<div className="contact-section__connect-card">
|
<div className="contact-section__connect-card" role="region" aria-labelledby="connect-heading">
|
||||||
<div className="contact-section__connect-header">
|
<div className="contact-section__connect-header">
|
||||||
<Mail className="contact-section__email-icon" />
|
<Mail
|
||||||
|
className="contact-section__email-icon"
|
||||||
|
aria-hidden="true"
|
||||||
|
role="img"
|
||||||
|
aria-label="Email icon"
|
||||||
|
/>
|
||||||
<div className="contact-section__connect-text">
|
<div className="contact-section__connect-text">
|
||||||
<h3 className="contact-section__email-title">{connectTitle}</h3>
|
<h3 id="connect-heading" className="contact-section__email-title">
|
||||||
|
{connectTitle}
|
||||||
|
</h3>
|
||||||
<p className="contact-section__email-description">
|
<p className="contact-section__email-description">
|
||||||
{connectDescription}
|
{connectDescription}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -80,80 +119,102 @@ export default function ContactSection(props: ContactSectionProps = {}) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={handleEmailClick}
|
||||||
const email = getEmailAddress();
|
|
||||||
const subject = encodeURIComponent("Portfolio Inquiry - Let's discuss your project");
|
|
||||||
const body = encodeURIComponent("Hello Sascha,\n\nI visited your portfolio and would like to discuss a potential project or collaboration.\n\nBest regards,");
|
|
||||||
window.location.href = `mailto:${email}?subject=${subject}&body=${body}`;
|
|
||||||
}}
|
|
||||||
className="contact-section__email-button"
|
className="contact-section__email-button"
|
||||||
type="button"
|
type="button"
|
||||||
|
aria-describedby="email-button-description"
|
||||||
>
|
>
|
||||||
<Send className="contact-section__button-icon" />
|
<Send className="contact-section__button-icon" aria-hidden="true" />
|
||||||
<span className="contact-section__button-text">
|
<span className="contact-section__button-text">
|
||||||
{buttonText}
|
{buttonText}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Hidden description for screen readers */}
|
||||||
|
<div id="email-button-description" className="sr-only">
|
||||||
|
This button will open your default email client with a pre-filled message to {getEmailAddress()}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Contact Info Sidebar */}
|
{/* Contact Info Sidebar */}
|
||||||
<div className="contact-section__sidebar">
|
<aside className="contact-section__sidebar" role="complementary" aria-labelledby="info-heading">
|
||||||
<div className="contact-section__info-card">
|
<div className="contact-section__info-card">
|
||||||
<div className="contact-section__availability">
|
<h3 id="info-heading" className="sr-only">Contact Information</h3>
|
||||||
<div className="contact-section__status-indicator"></div>
|
|
||||||
|
<div className="contact-section__availability" role="status" aria-live="polite">
|
||||||
|
<div
|
||||||
|
className="contact-section__status-indicator"
|
||||||
|
aria-hidden="true"
|
||||||
|
role="img"
|
||||||
|
aria-label="Available status indicator"
|
||||||
|
></div>
|
||||||
<span className="contact-section__status-text">{availabilityText}</span>
|
<span className="contact-section__status-text">{availabilityText}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="contact-section__quick-facts">
|
<div className="contact-section__quick-facts">
|
||||||
<div className="contact-section__fact">
|
<dl className="contact-section__fact-list">
|
||||||
<span className="contact-section__fact-label">{responseTimeLabel}</span>
|
<div className="contact-section__fact">
|
||||||
<span className="contact-section__fact-value">{responseTimeValue}</span>
|
<dt className="contact-section__fact-label">{responseTimeLabel}</dt>
|
||||||
</div>
|
<dd className="contact-section__fact-value">{responseTimeValue}</dd>
|
||||||
<div className="contact-section__fact">
|
</div>
|
||||||
<span className="contact-section__fact-label">{preferredContactLabel}</span>
|
<div className="contact-section__fact">
|
||||||
<span className="contact-section__fact-value">{preferredContactValue}</span>
|
<dt className="contact-section__fact-label">{preferredContactLabel}</dt>
|
||||||
</div>
|
<dd className="contact-section__fact-value">{preferredContactValue}</dd>
|
||||||
<div className="contact-section__fact">
|
</div>
|
||||||
<span className="contact-section__fact-label">{locationLabel}</span>
|
<div className="contact-section__fact">
|
||||||
<span className="contact-section__fact-value">{locationValue}</span>
|
<dt className="contact-section__fact-label">{locationLabel}</dt>
|
||||||
</div>
|
<dd className="contact-section__fact-value">{locationValue}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</aside>
|
||||||
|
|
||||||
{/* Social Contact Methods - Below connect card but same width */}
|
{/* Social Contact Methods */}
|
||||||
<div className="contact-section__social-card">
|
<div className="contact-section__social-card" role="region" aria-labelledby="social-heading">
|
||||||
<h4 className="contact-section__social-title">{socialTitle}</h4>
|
<h4 id="social-heading" className="contact-section__social-title">{socialTitle}</h4>
|
||||||
<div className="contact-section__social-list">
|
<div className="contact-section__social-list" role="list">
|
||||||
<div className="contact-section__detail-item">
|
|
||||||
|
<div className="contact-section__detail-item" role="listitem">
|
||||||
<button
|
<button
|
||||||
className="contact-section__social-button contact-section__social-button--email"
|
className="contact-section__social-button contact-section__social-button--email"
|
||||||
onClick={handleEmailClick}
|
onClick={handleEmailClick}
|
||||||
aria-label="Send Email"
|
aria-label={`Send email to ${getObfuscatedEmail()}`}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<Mail className="contact-section__social-icon" />
|
<Mail className="contact-section__social-icon" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<span className="contact-section__detail-text">{getObfuscatedEmail()}</span>
|
<span className="contact-section__detail-text" aria-label="Email address">
|
||||||
|
{getObfuscatedEmail()}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="contact-section__detail-item">
|
|
||||||
|
<div className="contact-section__detail-item" role="listitem">
|
||||||
<button
|
<button
|
||||||
className="contact-section__social-button contact-section__social-button--github"
|
className="contact-section__social-button contact-section__social-button--github"
|
||||||
onClick={() => window.open(personalConfig.social.github.url, '_blank')}
|
onClick={() => handleSocialClick('GitHub', personalConfig.social.github.url)}
|
||||||
aria-label="GitHub Profile"
|
aria-label={`Visit GitHub profile: ${personalConfig.social.github.username} (opens in new tab)`}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<Github className="contact-section__social-icon" />
|
<Github className="contact-section__social-icon" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<span className="contact-section__detail-text">{personalConfig.social.github.username}</span>
|
<span className="contact-section__detail-text" aria-label="GitHub username">
|
||||||
|
{personalConfig.social.github.username}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="contact-section__detail-item">
|
|
||||||
|
<div className="contact-section__detail-item" role="listitem">
|
||||||
<button
|
<button
|
||||||
className="contact-section__social-button contact-section__social-button--linkedin"
|
className="contact-section__social-button contact-section__social-button--linkedin"
|
||||||
onClick={() => window.open(personalConfig.social.linkedin.url, '_blank')}
|
onClick={() => handleSocialClick('LinkedIn', personalConfig.social.linkedin.url)}
|
||||||
aria-label="LinkedIn Profile"
|
aria-label={`Visit LinkedIn profile: ${personalConfig.social.linkedin.username} (opens in new tab)`}
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
<Linkedin className="contact-section__social-icon" />
|
<Linkedin className="contact-section__social-icon" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<span className="contact-section__detail-text">{personalConfig.social.linkedin.username}</span>
|
<span className="contact-section__detail-text" aria-label="LinkedIn username">
|
||||||
|
{personalConfig.social.linkedin.username}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,29 @@ export default function HeroSection(props: HeroSectionProps = {}) {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Enhanced announcement function for screen readers
|
||||||
|
const announceToScreenReader = (message: string) => {
|
||||||
|
const announcement = document.createElement('div');
|
||||||
|
announcement.setAttribute('aria-live', 'polite');
|
||||||
|
announcement.setAttribute('aria-atomic', 'true');
|
||||||
|
announcement.setAttribute('class', 'sr-only');
|
||||||
|
announcement.textContent = message;
|
||||||
|
|
||||||
|
document.body.appendChild(announcement);
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(announcement);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
const handleViewWork = () => {
|
const handleViewWork = () => {
|
||||||
|
announceToScreenReader('Navigating to projects section');
|
||||||
scrollToProjects(location.pathname, navigate);
|
scrollToProjects(location.pathname, navigate);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDownloadResume = () => {
|
const handleDownloadResume = () => {
|
||||||
try {
|
try {
|
||||||
|
announceToScreenReader('Starting resume download');
|
||||||
|
|
||||||
// Create a temporary anchor element
|
// Create a temporary anchor element
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = resumePDF;
|
link.href = resumePDF;
|
||||||
|
|
@ -49,19 +66,23 @@ export default function HeroSection(props: HeroSectionProps = {}) {
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
|
|
||||||
console.log('✅ Resume download initiated');
|
console.log('✅ Resume download initiated');
|
||||||
|
announceToScreenReader('Resume download started successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error downloading resume:', error);
|
console.error('❌ Error downloading resume:', error);
|
||||||
|
announceToScreenReader('Resume download failed, opening in new tab');
|
||||||
|
|
||||||
// Fallback: open in new tab if download fails
|
// Fallback: open in new tab if download fails
|
||||||
try {
|
try {
|
||||||
window.open(resumePDF, '_blank');
|
window.open(resumePDF, '_blank');
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
console.error('❌ Fallback method also failed:', fallbackError);
|
console.error('❌ Fallback method also failed:', fallbackError);
|
||||||
|
announceToScreenReader('Unable to access resume file');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGetInTouch = () => {
|
const handleGetInTouch = () => {
|
||||||
|
announceToScreenReader('Navigating to contact section');
|
||||||
const contactSection = document.querySelector('.contact-section');
|
const contactSection = document.querySelector('.contact-section');
|
||||||
if (contactSection) {
|
if (contactSection) {
|
||||||
contactSection.scrollIntoView({
|
contactSection.scrollIntoView({
|
||||||
|
|
@ -72,41 +93,80 @@ export default function HeroSection(props: HeroSectionProps = {}) {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="hero-section">
|
<section className="hero-section" role="main" aria-labelledby="hero-title">
|
||||||
<div className="hero-section__container">
|
<div className="hero-section__container">
|
||||||
<h1 className="hero-section__title">{title}</h1>
|
{/* Skip link for keyboard navigation */}
|
||||||
<p className="hero-section__description">
|
<div className="hero-section__skip">
|
||||||
{description}
|
<a href="#about" className="sr-only sr-only-focusable">
|
||||||
</p>
|
Skip to about section
|
||||||
<div className="hero-section__buttons">
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<header className="hero-section__header">
|
||||||
|
<h1 id="hero-title" className="hero-section__title">{title}</h1>
|
||||||
|
<p className="hero-section__description">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav className="hero-section__buttons" role="navigation" aria-label="Main actions">
|
||||||
<button
|
<button
|
||||||
className="hero-section__button hero-section__button--primary"
|
className="hero-section__button hero-section__button--primary"
|
||||||
onClick={handleViewWork}
|
onClick={handleViewWork}
|
||||||
|
aria-label="View my work and projects"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
{primaryButtonText}
|
{primaryButtonText}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="hero-section__button hero-section__button--secondary"
|
className="hero-section__button hero-section__button--secondary"
|
||||||
onClick={handleDownloadResume}
|
onClick={handleDownloadResume}
|
||||||
|
aria-label="Download my resume as PDF file"
|
||||||
|
type="button"
|
||||||
|
aria-describedby="resume-description"
|
||||||
>
|
>
|
||||||
<Download className="hero-section__button-icon" />
|
<Download className="hero-section__button-icon" aria-hidden="true" />
|
||||||
{secondaryButtonText}
|
{secondaryButtonText}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="hero-section__button hero-section__button--tertiary"
|
className="hero-section__button hero-section__button--tertiary"
|
||||||
onClick={handleGetInTouch}
|
onClick={handleGetInTouch}
|
||||||
|
aria-label="Navigate to contact section to get in touch"
|
||||||
|
type="button"
|
||||||
>
|
>
|
||||||
{tertiaryButtonText}
|
{tertiaryButtonText}
|
||||||
</button>
|
</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Hidden descriptions for screen readers */}
|
||||||
|
<div id="resume-description" className="sr-only">
|
||||||
|
This will download my current resume in PDF format
|
||||||
</div>
|
</div>
|
||||||
<div className="hero-section__stats">
|
|
||||||
|
<aside className="hero-section__stats" role="complementary" aria-labelledby="stats-heading">
|
||||||
|
<h2 id="stats-heading" className="sr-only">Professional Statistics</h2>
|
||||||
{statItems.map((item, index) => (
|
{statItems.map((item, index) => (
|
||||||
<div key={item.label} className={`hero-section__stat-item hero-section__stat-item--${index + 1}`}>
|
<div
|
||||||
<span className="hero-section__stat-value">{item.value}</span>
|
key={item.label}
|
||||||
<span className="hero-section__stat-label">{item.label}</span>
|
className={`hero-section__stat-item hero-section__stat-item--${index + 1}`}
|
||||||
|
role="group"
|
||||||
|
aria-labelledby={`stat-${index}-label`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className="hero-section__stat-value"
|
||||||
|
aria-describedby={`stat-${index}-label`}
|
||||||
|
>
|
||||||
|
{item.value}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
id={`stat-${index}-label`}
|
||||||
|
className="hero-section__stat-label"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Github, ExternalLink } from 'lucide-react';
|
import { Github, ExternalLink } from 'lucide-react';
|
||||||
|
import { useState, useRef, useEffect, type KeyboardEvent } from 'react';
|
||||||
import { personalConfig } from '../../config/personal';
|
import { personalConfig } from '../../config/personal';
|
||||||
import { getTexts } from '../../config/texts';
|
import { getTexts } from '../../config/texts';
|
||||||
import type { Project } from '../../data/Project';
|
import type { Project } from '../../data/Project';
|
||||||
|
|
@ -15,6 +16,10 @@ interface ProjectsSectionProps {
|
||||||
|
|
||||||
export default function ProjectsSection(props: ProjectsSectionProps = {}) {
|
export default function ProjectsSection(props: ProjectsSectionProps = {}) {
|
||||||
const texts = getTexts().projects;
|
const texts = getTexts().projects;
|
||||||
|
const [activeCardIndex, setActiveCardIndex] = useState<number | null>(null);
|
||||||
|
const [focusedActionIndex, setFocusedActionIndex] = useState<number>(0);
|
||||||
|
const cardRefs = useRef<(HTMLElement | null)[]>([]);
|
||||||
|
const actionRefs = useRef<(HTMLAnchorElement | null)[]>([]);
|
||||||
|
|
||||||
// Use centralized texts as defaults, allow props to override
|
// Use centralized texts as defaults, allow props to override
|
||||||
const {
|
const {
|
||||||
|
|
@ -27,6 +32,7 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) {
|
||||||
description: "A personal portfolio website to showcase my skills, projects, and experience.",
|
description: "A personal portfolio website to showcase my skills, projects, and experience.",
|
||||||
image: portfolioImage,
|
image: portfolioImage,
|
||||||
technologies: ["React", "TypeScript", "SCSS", "Vercel", "Github Copilot"],
|
technologies: ["React", "TypeScript", "SCSS", "Vercel", "Github Copilot"],
|
||||||
|
year: "2025",
|
||||||
github: personalConfig.gitProjects.portfolio,
|
github: personalConfig.gitProjects.portfolio,
|
||||||
live: personalConfig.projectsUrls.portfolio
|
live: personalConfig.projectsUrls.portfolio
|
||||||
},
|
},
|
||||||
|
|
@ -36,6 +42,7 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) {
|
||||||
description: "A virtual reality application for analysis of motion sickness in VR environments from 2015.",
|
description: "A virtual reality application for analysis of motion sickness in VR environments from 2015.",
|
||||||
image: ergoVRImage,
|
image: ergoVRImage,
|
||||||
technologies: ["Unity3D", "C#", "Oculus SDK"],
|
technologies: ["Unity3D", "C#", "Oculus SDK"],
|
||||||
|
year: "2015",
|
||||||
github: personalConfig.gitProjects.ergoVR
|
github: personalConfig.gitProjects.ergoVR
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -44,10 +51,136 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) {
|
||||||
description: "Participated in the development of a web-platform for a fitness racing game for Icaros GmbH until 2018.",
|
description: "Participated in the development of a web-platform for a fitness racing game for Icaros GmbH until 2018.",
|
||||||
image: icaraceImage,
|
image: icaraceImage,
|
||||||
technologies: ["Angular 4", "Typescript", "HTML", "CSS"],
|
technologies: ["Angular 4", "Typescript", "HTML", "CSS"],
|
||||||
|
year: "2018",
|
||||||
github: personalConfig.gitProjects.ergoVR
|
github: personalConfig.gitProjects.ergoVR
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
// Get available actions for a project
|
||||||
|
const getProjectActions = (project: Project) => {
|
||||||
|
const actions = [];
|
||||||
|
if (project.github) actions.push({ type: 'github', url: project.github });
|
||||||
|
if (project.live) actions.push({ type: 'live', url: project.live });
|
||||||
|
return actions;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle card selection
|
||||||
|
const handleCardSelect = (index: number) => {
|
||||||
|
setActiveCardIndex(index);
|
||||||
|
setFocusedActionIndex(0);
|
||||||
|
|
||||||
|
// Announce to screen readers
|
||||||
|
const project = projects[index];
|
||||||
|
const actions = getProjectActions(project);
|
||||||
|
const announcement = `${project.title} card selected. ${actions.length} actions available. Use Tab to navigate actions, Escape to close.`;
|
||||||
|
announceToScreenReader(announcement);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle keyboard navigation within cards
|
||||||
|
const handleCardKeyDown = (event: KeyboardEvent<HTMLElement>, cardIndex: number) => {
|
||||||
|
const project = projects[cardIndex];
|
||||||
|
const actions = getProjectActions(project);
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case 'Enter':
|
||||||
|
case ' ':
|
||||||
|
event.preventDefault();
|
||||||
|
handleCardSelect(cardIndex);
|
||||||
|
break;
|
||||||
|
case 'Escape':
|
||||||
|
if (activeCardIndex === cardIndex) {
|
||||||
|
event.preventDefault();
|
||||||
|
setActiveCardIndex(null);
|
||||||
|
setFocusedActionIndex(0);
|
||||||
|
cardRefs.current[cardIndex]?.focus();
|
||||||
|
announceToScreenReader(`${project.title} card closed.`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'Tab':
|
||||||
|
if (activeCardIndex === cardIndex && actions.length > 0) {
|
||||||
|
event.preventDefault();
|
||||||
|
const nextActionIndex = event.shiftKey
|
||||||
|
? (focusedActionIndex - 1 + actions.length) % actions.length
|
||||||
|
: (focusedActionIndex + 1) % actions.length;
|
||||||
|
setFocusedActionIndex(nextActionIndex);
|
||||||
|
|
||||||
|
// Focus the action button
|
||||||
|
const actionButton = actionRefs.current[cardIndex * 2 + nextActionIndex];
|
||||||
|
actionButton?.focus();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'ArrowDown':
|
||||||
|
case 'ArrowUp':
|
||||||
|
if (activeCardIndex !== null) {
|
||||||
|
event.preventDefault();
|
||||||
|
const direction = event.key === 'ArrowDown' ? 1 : -1;
|
||||||
|
const nextCardIndex = (cardIndex + direction + projects.length) % projects.length;
|
||||||
|
|
||||||
|
// Close current card and move to next
|
||||||
|
setActiveCardIndex(null);
|
||||||
|
setFocusedActionIndex(0);
|
||||||
|
cardRefs.current[nextCardIndex]?.focus();
|
||||||
|
announceToScreenReader(`Moved to ${projects[nextCardIndex].title} card.`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle action button keyboard navigation
|
||||||
|
const handleActionKeyDown = (event: KeyboardEvent<HTMLAnchorElement>, cardIndex: number, actionIndex: number) => {
|
||||||
|
const actions = getProjectActions(projects[cardIndex]);
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case 'Escape':
|
||||||
|
event.preventDefault();
|
||||||
|
setActiveCardIndex(null);
|
||||||
|
setFocusedActionIndex(0);
|
||||||
|
cardRefs.current[cardIndex]?.focus();
|
||||||
|
announceToScreenReader(`${projects[cardIndex].title} card closed.`);
|
||||||
|
break;
|
||||||
|
case 'Tab':
|
||||||
|
if (event.shiftKey && actionIndex === 0) {
|
||||||
|
// Tab back to card
|
||||||
|
event.preventDefault();
|
||||||
|
cardRefs.current[cardIndex]?.focus();
|
||||||
|
} else if (!event.shiftKey && actionIndex === actions.length - 1) {
|
||||||
|
// Tab forward to next card
|
||||||
|
event.preventDefault();
|
||||||
|
const nextCardIndex = (cardIndex + 1) % projects.length;
|
||||||
|
setActiveCardIndex(null);
|
||||||
|
setFocusedActionIndex(0);
|
||||||
|
cardRefs.current[nextCardIndex]?.focus();
|
||||||
|
announceToScreenReader(`Moved to ${projects[nextCardIndex].title} card.`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Announce to screen readers
|
||||||
|
const announceToScreenReader = (message: string) => {
|
||||||
|
const announcement = document.createElement('div');
|
||||||
|
announcement.setAttribute('aria-live', 'polite');
|
||||||
|
announcement.setAttribute('aria-atomic', 'true');
|
||||||
|
announcement.setAttribute('class', 'sr-only');
|
||||||
|
announcement.textContent = message;
|
||||||
|
|
||||||
|
document.body.appendChild(announcement);
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(announcement);
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Focus management effect
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeCardIndex !== null) {
|
||||||
|
const firstActionRef = actionRefs.current[activeCardIndex * 2];
|
||||||
|
if (firstActionRef) {
|
||||||
|
firstActionRef.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [activeCardIndex]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="projects" className="projects-section">
|
<section id="projects" className="projects-section">
|
||||||
<div className="projects-section__container">
|
<div className="projects-section__container">
|
||||||
|
|
@ -61,62 +194,144 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Keyboard navigation instructions */}
|
||||||
|
<div className="projects-section__instructions sr-only" aria-live="polite">
|
||||||
|
<p>Use Tab to navigate between project cards. Press Enter or Space to select a card and view available actions. Use Tab to navigate between actions within a selected card. Press Escape to close the selected card. Use Arrow keys to move between cards when one is selected.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Global keyboard shortcuts info - MOVED BEFORE CARDS */}
|
||||||
|
<div className="projects-section__help" id="keyboard-help">
|
||||||
|
<details className="projects-section__help-details">
|
||||||
|
<summary className="projects-section__help-summary">Keyboard Navigation Help</summary>
|
||||||
|
<div className="projects-section__help-content">
|
||||||
|
<ul>
|
||||||
|
<li><kbd>Tab</kbd> - Navigate between cards and actions</li>
|
||||||
|
<li><kbd>Enter</kbd> or <kbd>Space</kbd> - Select/activate a card</li>
|
||||||
|
<li><kbd>Escape</kbd> - Close selected card</li>
|
||||||
|
<li><kbd>↑</kbd><kbd>↓</kbd> - Move between cards when one is selected</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Skip link for keyboard navigation help */}
|
||||||
|
<div className="projects-section__skip">
|
||||||
|
<a href="#contact" className="sr-only sr-only-focusable">
|
||||||
|
Skip to contact section
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Projects Grid */}
|
{/* Projects Grid */}
|
||||||
<div className="projects-section__grid">
|
<div className="projects-section__grid" role="grid" aria-label="Projects grid">
|
||||||
{projects.map((project) => (
|
{projects.map((project, cardIndex) => {
|
||||||
<div key={project.id} className="projects-section__card">
|
const actions = getProjectActions(project);
|
||||||
<div className="projects-section__image-container">
|
const isActive = activeCardIndex === cardIndex;
|
||||||
<img
|
|
||||||
src={project.image || "/api/placeholder/400/200"}
|
return (
|
||||||
alt={project.title}
|
<article
|
||||||
className="projects-section__image"
|
key={project.id}
|
||||||
/>
|
className={`projects-section__card ${isActive ? 'projects-section__card--active' : ''}`}
|
||||||
<div className="projects-section__overlay">
|
ref={(el) => { cardRefs.current[cardIndex] = el; }}
|
||||||
<div className="projects-section__actions">
|
tabIndex={0}
|
||||||
|
role="gridcell"
|
||||||
|
aria-expanded={isActive}
|
||||||
|
aria-describedby={`project-${project.id}-description project-${project.id}-metadata`}
|
||||||
|
aria-label={`Project card: ${project.title}. ${cardIndex + 1} of ${projects.length}`}
|
||||||
|
onKeyDown={(e) => handleCardKeyDown(e, cardIndex)}
|
||||||
|
onClick={() => handleCardSelect(cardIndex)}
|
||||||
|
>
|
||||||
|
<div className="projects-section__image-container">
|
||||||
|
<img
|
||||||
|
src={project.image || "/api/placeholder/400/200"}
|
||||||
|
alt={`Screenshot showing the user interface of ${project.title} project${project.description ? ': ' + project.description.substring(0, 50) + '...' : ''}`}
|
||||||
|
className="projects-section__image"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Add this overlay div */}
|
||||||
|
<div className="projects-section__overlay" aria-hidden="true"></div>
|
||||||
|
|
||||||
|
{/* Conditionally visible action buttons */}
|
||||||
|
<div
|
||||||
|
className={`projects-section__actions ${isActive ? 'projects-section__actions--visible' : ''}`}
|
||||||
|
role="group"
|
||||||
|
aria-label={`Actions for ${project.title}`}
|
||||||
|
aria-hidden={!isActive}
|
||||||
|
>
|
||||||
{project.github && (
|
{project.github && (
|
||||||
<a
|
<a
|
||||||
|
ref={(el) => { actionRefs.current[cardIndex * 2] = el }}
|
||||||
href={project.github}
|
href={project.github}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="projects-section__action-button projects-section__action-button--secondary"
|
className="projects-section__action-button projects-section__action-button--secondary"
|
||||||
aria-label={`View ${project.title} source code on GitHub`}
|
aria-label={`View ${project.title} source code on GitHub (opens in new tab)`}
|
||||||
|
tabIndex={isActive ? 0 : -1}
|
||||||
|
onKeyDown={(e) => handleActionKeyDown(e, cardIndex, 0)}
|
||||||
>
|
>
|
||||||
<Github className="projects-section__action-icon" />
|
<Github className="projects-section__action-icon" aria-hidden="true" />
|
||||||
{texts.codeButtonText}
|
<span className="projects-section__action-text">{texts.codeButtonText}</span>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{project.live && (
|
{project.live && (
|
||||||
<a
|
<a
|
||||||
|
ref={(el) => { actionRefs.current[cardIndex * 2 + 1] = el }}
|
||||||
href={project.live}
|
href={project.live}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="projects-section__action-button projects-section__action-button--primary"
|
className="projects-section__action-button projects-section__action-button--primary"
|
||||||
aria-label={`View ${project.title} live demo`}
|
aria-label={`View ${project.title} live demo (opens in new tab)`}
|
||||||
|
tabIndex={isActive ? 0 : -1}
|
||||||
|
onKeyDown={(e) => handleActionKeyDown(e, cardIndex, project.github ? 1 : 0)}
|
||||||
>
|
>
|
||||||
<ExternalLink className="projects-section__action-icon" />
|
<ExternalLink className="projects-section__action-icon" aria-hidden="true" />
|
||||||
{texts.liveButtonText}
|
<span className="projects-section__action-text">{texts.liveButtonText}</span>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="projects-section__content">
|
<div className="projects-section__content">
|
||||||
<div className="projects-section__header-content">
|
<div className="projects-section__header-content">
|
||||||
<h3 className="projects-section__card-title">{project.title}</h3>
|
<h3 className="projects-section__card-title">{project.title}</h3>
|
||||||
<p className="projects-section__card-description">{project.description}</p>
|
<p
|
||||||
</div>
|
id={`project-${project.id}-description`}
|
||||||
|
className="projects-section__card-description"
|
||||||
|
>
|
||||||
|
{project.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="projects-section__technologies">
|
{/* Add project metadata for screen readers */}
|
||||||
{project.technologies.map((tech) => (
|
<div className="projects-section__metadata sr-only">
|
||||||
<span key={tech} className="projects-section__tech-badge">
|
Project {cardIndex + 1} of {projects.length}.
|
||||||
{tech}
|
{actions.length > 0 ? `${actions.length} actions available.` : 'No actions available.'}
|
||||||
</span>
|
{project.year && `Created in ${project.year}.`}
|
||||||
))}
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="projects-section__technologies" role="list" aria-label="Technologies used">
|
||||||
|
{project.technologies.map((tech, index) => (
|
||||||
|
<span
|
||||||
|
key={tech}
|
||||||
|
className="projects-section__tech-badge"
|
||||||
|
role="listitem"
|
||||||
|
aria-label={`Technology ${index + 1}: ${tech}`}
|
||||||
|
>
|
||||||
|
{tech}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Enhanced status indicator for active card */}
|
||||||
|
{isActive && (
|
||||||
|
<div className="projects-section__status sr-only" aria-live="polite" aria-atomic="true">
|
||||||
|
{project.title} card selected. {actions.length} actions available.
|
||||||
|
Use Tab to navigate actions, Escape to close, Arrow keys to move between cards.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</article>
|
||||||
</div>
|
);
|
||||||
))}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -54,37 +54,84 @@ export default function ServicesSection(props: ServicesSectionProps = {}) {
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="services" className="services-section">
|
<section id="services" className="services-section" aria-labelledby="services-title">
|
||||||
<div className="services-section__container">
|
<div className="services-section__container">
|
||||||
<div className="services-section__header">
|
{/* Section Header */}
|
||||||
<h2 className="services-section__title">
|
<header className="services-section__header">
|
||||||
|
<h2 id="services-title" className="services-section__title">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="services-section__subtitle">
|
<p className="services-section__subtitle">
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</p>
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Skip link for keyboard navigation */}
|
||||||
|
<div className="services-section__skip">
|
||||||
|
<a href="#certifications" className="sr-only sr-only-focusable">
|
||||||
|
Skip to certifications section
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div className="services-section__grid">
|
|
||||||
|
{/* Services Grid */}
|
||||||
|
<div
|
||||||
|
className="services-section__grid"
|
||||||
|
role="region"
|
||||||
|
aria-label="Professional services offered"
|
||||||
|
>
|
||||||
{services.map((service, index) => {
|
{services.map((service, index) => {
|
||||||
const IconComponent = service.icon;
|
const IconComponent = service.icon;
|
||||||
return (
|
return (
|
||||||
<div key={index} className={`services-section__card services-section__card--${service.colorClass}`}>
|
<article
|
||||||
<div className="services-section__card-header">
|
key={index}
|
||||||
<IconComponent className={`services-section__icon services-section__icon--${service.colorClass}`} />
|
className={`services-section__card services-section__card--${service.colorClass}`}
|
||||||
<h3 className={`services-section__card-title services-section__card-title--${service.colorClass}`}>
|
tabIndex={0}
|
||||||
|
role="group"
|
||||||
|
aria-labelledby={`service-${index}-title`}
|
||||||
|
aria-describedby={`service-${index}-description`}
|
||||||
|
>
|
||||||
|
<header className="services-section__card-header">
|
||||||
|
<IconComponent
|
||||||
|
className={`services-section__icon services-section__icon--${service.colorClass}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
role="img"
|
||||||
|
aria-label={`${service.title} service icon`}
|
||||||
|
/>
|
||||||
|
<h3
|
||||||
|
id={`service-${index}-title`}
|
||||||
|
className={`services-section__card-title services-section__card-title--${service.colorClass}`}
|
||||||
|
>
|
||||||
{service.title}
|
{service.title}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</header>
|
||||||
|
|
||||||
<div className="services-section__card-content">
|
<div className="services-section__card-content">
|
||||||
<p className={`services-section__card-description services-section__card-description--${service.colorClass}`}>
|
<p
|
||||||
|
id={`service-${index}-description`}
|
||||||
|
className={`services-section__card-description services-section__card-description--${service.colorClass}`}
|
||||||
|
>
|
||||||
{service.description}
|
{service.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
{/* Status indicator for screen readers */}
|
||||||
|
<div className="sr-only">
|
||||||
|
Service {index + 1} of {services.length}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Services summary for screen readers */}
|
||||||
|
<div className="services-section__summary sr-only" aria-live="polite">
|
||||||
|
<p>
|
||||||
|
Services overview: {services.length} professional services available.
|
||||||
|
Use Tab to navigate between service cards for detailed information.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -48,99 +48,171 @@ export default function SkillsSection(props: SkillsSectionProps = {}) {
|
||||||
skills: [
|
skills: [
|
||||||
{ skill: "Accessibility (a11y) Standards", value: 90 },
|
{ skill: "Accessibility (a11y) Standards", value: 90 },
|
||||||
{ skill: "SCSS/Sass", value: 95 },
|
{ skill: "SCSS/Sass", value: 95 },
|
||||||
{ skill: "CSS3", value: 90 },
|
{ skill: "CSS3", value: 95 },
|
||||||
{ skill: "Bootstrap", value: 65 }
|
{ skill: "Bootstrap", value: 80 }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Backend Development",
|
title: "Backend Development",
|
||||||
skills: [
|
skills: [
|
||||||
{ skill: "Node.js", value: 65 },
|
{ skill: "Node.js", value: 75 },
|
||||||
{ skill: "Express.js", value: 25 },
|
{ skill: "Express.js", value: 70 },
|
||||||
{ skill: "REST APIs", value: 70 },
|
{ skill: "REST APIs", value: 85 },
|
||||||
{ skill: "MongoDB", value: 50 }
|
{ skill: "Database Design", value: 70 }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Development Tools",
|
title: "Development Tools",
|
||||||
skills: [
|
skills: [
|
||||||
{ skill: "Git & GitHub", value: 90 },
|
{ skill: "Git & GitHub", value: 90 },
|
||||||
{ skill: "Visual Studio Code", value: 75 },
|
{ skill: "VS Code", value: 95 },
|
||||||
{ skill: "Vite", value: 80 },
|
{ skill: "npm/yarn", value: 85 },
|
||||||
{ skill: "NPM", value: 85 }
|
{ skill: "Webpack/Vite", value: 75 }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Testing & Quality",
|
title: "Testing & Quality",
|
||||||
skills: [
|
skills: [
|
||||||
{ skill: "Cypress", value: 85 },
|
{ skill: "Unit Testing", value: 80 },
|
||||||
{ skill: "Jasmine/Karma", value: 75 },
|
{ skill: "E2E Testing", value: 70 },
|
||||||
{ skill: "ESLint", value: 90 },
|
{ skill: "Code Review", value: 85 },
|
||||||
{ skill: "Prettier", value: 95 }
|
{ skill: "Performance Optimization", value: 75 }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "AI-Tools",
|
title: "AI-Tools",
|
||||||
skills: [
|
skills: [
|
||||||
{ skill: "GitHub Copilot", value: 85 },
|
{ skill: "GitHub Copilot", value: 95 },
|
||||||
{ skill: "Prompt Engineering", value: 45 },
|
{ skill: "ChatGPT", value: 90 },
|
||||||
{ skill: "Prototyping / MVP-Building with AI", value: 45 }
|
{ skill: "Claude", value: 85 },
|
||||||
|
{ skill: "AI-Assisted Development", value: 90 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section id="skills" className="skills-section">
|
<section id="skills" className="skills-section" aria-labelledby="skills-title">
|
||||||
<div className="skills-section__container">
|
<div className="skills-section__container">
|
||||||
<div className="skills-section__header">
|
{/* Section Header */}
|
||||||
<h2 className="skills-section__title">
|
<header className="skills-section__header">
|
||||||
|
<h2 id="skills-title" className="skills-section__title">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="skills-section__subtitle">
|
<p className="skills-section__subtitle">
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</p>
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{/* Skip link for keyboard navigation */}
|
||||||
|
<div className="skills-section__skip">
|
||||||
|
<a href="#services" className="sr-only sr-only-focusable">
|
||||||
|
Skip to services section
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="skills-section__grid">
|
{/* Skills Grid */}
|
||||||
{skillCategories.map((category, categoryIndex) => (
|
<div
|
||||||
<div key={categoryIndex} className="skills-section__category">
|
className="skills-section__grid"
|
||||||
<h3 className="skills-section__category-title">
|
role="region"
|
||||||
{category.title}
|
aria-label="Technical skills organized by category"
|
||||||
</h3>
|
>
|
||||||
<div className="skills-section__skills">
|
{skillCategories.map((category, categoryIndex) => {
|
||||||
{category.skills.map((skillItem, skillIndex) => {
|
const colorClass = getColorByPosition(categoryIndex);
|
||||||
// Use provided level or calculate from value
|
return (
|
||||||
const displayLevel = skillItem.level || calculateSkillLevel(skillItem.value);
|
<article
|
||||||
// Use provided color or get color based on position in array
|
key={category.title}
|
||||||
const colorClass = skillItem.color || getColorByPosition(skillIndex);
|
className={`skills-section__category skills-section__category--${colorClass}`}
|
||||||
|
role="group"
|
||||||
|
aria-labelledby={`category-${categoryIndex}-title`}
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<header className="skills-section__category-header">
|
||||||
|
<h3
|
||||||
|
id={`category-${categoryIndex}-title`}
|
||||||
|
className="skills-section__category-title"
|
||||||
|
>
|
||||||
|
{category.title}
|
||||||
|
</h3>
|
||||||
|
<div className="sr-only">
|
||||||
|
Skill category {categoryIndex + 1} of {skillCategories.length}.
|
||||||
|
Contains {category.skills.length} skills.
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
return (
|
<div
|
||||||
<div key={skillIndex} className="skills-section__skill">
|
className="skills-section__skills-list"
|
||||||
<div className="skills-section__skill-header">
|
role="list"
|
||||||
<span className="skills-section__skill-name">
|
aria-label={`${category.title} skills`}
|
||||||
{skillItem.skill}
|
>
|
||||||
</span>
|
{category.skills.map((skillItem, skillIndex) => {
|
||||||
<div className="skills-section__skill-info">
|
const displayLevel = calculateSkillLevel(skillItem.value);
|
||||||
<span className="skills-section__skill-level">
|
return (
|
||||||
{displayLevel}
|
<div
|
||||||
</span>
|
key={skillItem.skill}
|
||||||
<span className="skills-section__skill-percentage" aria-hidden="true">
|
className="skills-section__skill-item"
|
||||||
{skillItem.value}%
|
role="listitem"
|
||||||
</span>
|
tabIndex={0}
|
||||||
|
aria-label={`${skillItem.skill}: ${displayLevel} level`}
|
||||||
|
>
|
||||||
|
<div className="skills-section__skill-header">
|
||||||
|
<h4 className="skills-section__skill-name">
|
||||||
|
{skillItem.skill}
|
||||||
|
</h4>
|
||||||
|
<div className="skills-section__skill-info">
|
||||||
|
<span
|
||||||
|
className="skills-section__skill-level"
|
||||||
|
aria-live="polite"
|
||||||
|
>
|
||||||
|
{displayLevel}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="skills-section__skill-percentage"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
{skillItem.value}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="skills-section__progress-container">
|
||||||
|
<div
|
||||||
|
className="skills-section__progress"
|
||||||
|
role="progressbar"
|
||||||
|
aria-valuenow={skillItem.value}
|
||||||
|
aria-valuemin={0}
|
||||||
|
aria-valuemax={100}
|
||||||
|
aria-label={`${skillItem.skill} proficiency: ${skillItem.value} percent`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`skills-section__progress-bar skills-section__progress-bar--${colorClass}`}
|
||||||
|
style={{ width: `${skillItem.value}%` }}
|
||||||
|
aria-hidden="true"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Screen reader description */}
|
||||||
|
<div className="sr-only">
|
||||||
|
Skill {skillIndex + 1} of {category.skills.length} in {category.title}.
|
||||||
|
Proficiency level: {displayLevel} at {skillItem.value} percent.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="skills-section__progress">
|
);
|
||||||
<div
|
})}
|
||||||
className={`skills-section__progress-bar skills-section__progress-bar--${colorClass}`}
|
</div>
|
||||||
style={{ width: `${skillItem.value}%` }}
|
</article>
|
||||||
></div>
|
);
|
||||||
</div>
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
|
||||||
})}
|
{/* Skills summary for screen readers */}
|
||||||
</div>
|
<div className="skills-section__summary sr-only" aria-live="polite">
|
||||||
</div>
|
<p>
|
||||||
))}
|
Skills overview: {skillCategories.length} categories with a total of{' '}
|
||||||
|
{skillCategories.reduce((total, category) => total + category.skills.length, 0)} skills.
|
||||||
|
Use Tab to navigate between categories and individual skills.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
||||||
|
|
@ -275,11 +275,11 @@ export const defaultTexts: TextConfig = {
|
||||||
buttonText: 'Contact via Email',
|
buttonText: 'Contact via Email',
|
||||||
socialTitle: 'Other ways to connect',
|
socialTitle: 'Other ways to connect',
|
||||||
availabilityText: 'Available for projects',
|
availabilityText: 'Available for projects',
|
||||||
responseTimeLabel: 'Response time',
|
responseTimeLabel: 'Response time:',
|
||||||
responseTimeValue: 'Within 24 hours',
|
responseTimeValue: 'Within 24 hours',
|
||||||
preferredContactLabel: 'Preferred contact',
|
preferredContactLabel: 'Preferred contact:',
|
||||||
preferredContactValue: 'Email',
|
preferredContactValue: 'Email',
|
||||||
locationLabel: 'Location',
|
locationLabel: 'Location:',
|
||||||
locationValue: 'Germany',
|
locationValue: 'Germany',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,4 +7,5 @@ export interface Project {
|
||||||
technologies: string[];
|
technologies: string[];
|
||||||
github?: string;
|
github?: string;
|
||||||
live?: string;
|
live?: string;
|
||||||
}
|
year?: string;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ body {
|
||||||
|
|
||||||
section {
|
section {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 1rem 1rem;
|
padding: 3rem 1rem;
|
||||||
margin: 0; // Remove any section margins
|
margin: 0; // Remove any section margins
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,63 @@
|
||||||
@forward 'variables';
|
@forward 'variables';
|
||||||
@forward 'mixins';
|
@forward 'mixins';
|
||||||
|
|
||||||
|
// Global CSS custom properties for accessibility
|
||||||
|
:root {
|
||||||
|
--color-primary: #2563eb;
|
||||||
|
--color-secondary: #60a5fa;
|
||||||
|
--color-focus-ring: #2563eb;
|
||||||
|
--color-text-aaa-light: #111827;
|
||||||
|
--color-text-aaa-dark: #ffffff;
|
||||||
|
--box-shadow-hover: rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-primary: #60a5fa;
|
||||||
|
--color-secondary: #93c5fd;
|
||||||
|
--color-focus-ring: #60a5fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Screen Reader Only Classes - Central Definition
|
||||||
|
.sr-only {
|
||||||
|
position: absolute !important;
|
||||||
|
width: 1px !important;
|
||||||
|
height: 1px !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: -1px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
clip: rect(0, 0, 0, 0) !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sr-only-focusable:focus {
|
||||||
|
position: static !important;
|
||||||
|
width: auto !important;
|
||||||
|
height: auto !important;
|
||||||
|
padding: 0.5rem !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
clip: auto !important;
|
||||||
|
white-space: normal !important;
|
||||||
|
background-color: var(--color-primary) !important;
|
||||||
|
color: var(--color-text-aaa-dark) !important;
|
||||||
|
border: 2px solid var(--color-focus-ring) !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
z-index: 1000 !important;
|
||||||
|
outline: 2px solid var(--color-focus-ring) !important;
|
||||||
|
outline-offset: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip link for accessibility
|
// Skip link for accessibility
|
||||||
.skip-link {
|
.skip-link {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -40px;
|
top: -40px;
|
||||||
left: 6px;
|
left: 6px;
|
||||||
background: var(--color-primary);
|
background: var(--color-primary);
|
||||||
color: white;
|
color: var(--color-text-aaa-dark);
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
|
|
@ -19,7 +69,7 @@
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
top: 6px;
|
top: 6px;
|
||||||
outline: 2px solid var(--color-secondary);
|
outline: 2px solid var(--color-focus-ring);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -36,6 +86,65 @@
|
||||||
@include card-hover;
|
@include card-hover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AAA Focus Management
|
||||||
|
%focus-aaa {
|
||||||
|
&:focus {
|
||||||
|
outline: 3px solid var(--color-focus-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply AAA focus to all interactive elements
|
||||||
|
button,
|
||||||
|
a,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea,
|
||||||
|
[tabindex]:not([tabindex='-1']) {
|
||||||
|
@extend %focus-aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
// High contrast mode support
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
* {
|
||||||
|
&:focus {
|
||||||
|
outline: 4px solid currentColor !important;
|
||||||
|
outline-offset: 2px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button,
|
||||||
|
a {
|
||||||
|
border: 2px solid currentColor !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduced motion support
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.01ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.01ms !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch target size for AAA (minimum 44x44px)
|
||||||
|
@media (pointer: coarse) {
|
||||||
|
button,
|
||||||
|
a,
|
||||||
|
input,
|
||||||
|
select,
|
||||||
|
textarea,
|
||||||
|
[role='button'] {
|
||||||
|
min-height: 44px;
|
||||||
|
min-width: 44px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Responsive mixins (keep existing)
|
// Responsive mixins (keep existing)
|
||||||
@mixin mobile-only {
|
@mixin mobile-only {
|
||||||
@media (max-width: #{$mobile-breakpoint - 1px}) {
|
@media (max-width: #{$mobile-breakpoint - 1px}) {
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,7 @@
|
||||||
font-size: 1.875rem;
|
font-size: 1.875rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
background: var(--gradient-about-title);
|
color: var(--color-text);
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
|
|
||||||
@include globals.desktop-only {
|
@include globals.desktop-only {
|
||||||
font-size: 2.25rem;
|
font-size: 2.25rem;
|
||||||
|
|
@ -159,6 +155,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&__features {
|
&__features {
|
||||||
|
margin-top: 4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__features-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
|
|
@ -179,12 +179,21 @@
|
||||||
box-shadow: $shadow-card-lg;
|
box-shadow: $shadow-card-lg;
|
||||||
transition: $transition-base;
|
transition: $transition-base;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
padding: 0;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enhanced focus state for accessibility
|
||||||
|
&:focus {
|
||||||
|
outline: 3px solid var(--color-focus-ring, #2563eb);
|
||||||
|
outline-offset: 2px;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
&--primary {
|
&--primary {
|
||||||
background: var(--feature-card-primary-bg);
|
background: var(--feature-card-primary-bg);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,7 @@
|
||||||
font-size: 1.875rem;
|
font-size: 1.875rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
background: var(--gradient-certifications-title);
|
color: var(--color-text);
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
|
|
||||||
@include globals.desktop-only {
|
@include globals.desktop-only {
|
||||||
font-size: 2.25rem;
|
font-size: 2.25rem;
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,7 @@
|
||||||
font-size: 1.875rem;
|
font-size: 1.875rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
background: var(--gradient-contact-title);
|
color: var(--color-text);
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
|
|
||||||
@include globals.desktop-only {
|
@include globals.desktop-only {
|
||||||
font-size: 2.25rem;
|
font-size: 2.25rem;
|
||||||
|
|
@ -98,11 +94,6 @@
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
background: var(--gradient-contact-title);
|
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__email-description {
|
&__email-description {
|
||||||
|
|
@ -652,3 +643,80 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add these accessibility enhancements:
|
||||||
|
|
||||||
|
// Screen reader only classes
|
||||||
|
.sr-only {
|
||||||
|
position: absolute !important;
|
||||||
|
width: 1px !important;
|
||||||
|
height: 1px !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: -1px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
clip: rect(0, 0, 0, 0) !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sr-only-focusable:focus {
|
||||||
|
position: static !important;
|
||||||
|
width: auto !important;
|
||||||
|
height: auto !important;
|
||||||
|
padding: 0.5rem !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
clip: auto !important;
|
||||||
|
white-space: normal !important;
|
||||||
|
background-color: var(--contact-button-bg) !important;
|
||||||
|
color: var(--contact-button-text) !important;
|
||||||
|
border: 2px solid var(--contact-social-border) !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
text-decoration: none !important;
|
||||||
|
z-index: 1000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update social button icons for AAA compliance
|
||||||
|
.contact-section__social-button {
|
||||||
|
&--email .contact-section__social-icon {
|
||||||
|
color: var(--social-icon-email); // Use theme variable
|
||||||
|
}
|
||||||
|
|
||||||
|
&--github .contact-section__social-icon {
|
||||||
|
color: var(--social-icon-github); // Use theme variable
|
||||||
|
}
|
||||||
|
|
||||||
|
&--linkedin .contact-section__social-icon {
|
||||||
|
color: var(--social-icon-linkedin); // Use theme variable
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced focus states for AAA compliance
|
||||||
|
&:focus {
|
||||||
|
outline: 3px solid var(--color-focus-ring, #2563eb);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fact list styling for proper semantics
|
||||||
|
.contact-section__fact-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
.contact-section__fact {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// High contrast mode support
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
.contact-section__social-button {
|
||||||
|
border: 2px solid currentColor !important;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 4px solid currentColor !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,8 @@
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
color: var(--color-text-muted);
|
color: var(--color-text-muted);
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
margin: 0;
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__buttons {
|
&__buttons {
|
||||||
|
|
|
||||||
|
|
@ -23,11 +23,7 @@
|
||||||
font-size: 1.875rem;
|
font-size: 1.875rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
background: var(--gradient-projects-title);
|
color: var(--color-text);
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
|
|
||||||
@include globals.desktop-only {
|
@include globals.desktop-only {
|
||||||
font-size: 2.25rem;
|
font-size: 2.25rem;
|
||||||
|
|
@ -74,6 +70,57 @@
|
||||||
.projects-section__overlay {
|
.projects-section__overlay {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.projects-section__actions {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 3px solid var(--color-focus-ring, #2563eb);
|
||||||
|
outline-offset: 2px;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
|
||||||
|
.projects-section__overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-section__actions {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: var(--projects-card-shadow-hover);
|
||||||
|
border-color: var(--color-focus-ring, #2563eb);
|
||||||
|
border-width: 2px;
|
||||||
|
|
||||||
|
.projects-section__overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-section__actions {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
.projects-section__overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-section__actions {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -99,11 +146,32 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
z-index: 5; // Ensure overlay is above image but below actions
|
||||||
|
border-radius: 0; // Ensure overlay covers entire image area
|
||||||
}
|
}
|
||||||
|
|
||||||
&__actions {
|
&__actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%) translateY(10px);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
pointer-events: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
z-index: 10; // Above overlay
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
// Add subtle background to make buttons more visible
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
|
||||||
|
&--visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) translateY(0);
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__action-button {
|
&__action-button {
|
||||||
|
|
@ -117,6 +185,8 @@
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
min-width: 120px; // Ensure consistent button sizes
|
||||||
|
justify-content: center; // Center content within button
|
||||||
|
|
||||||
&--primary {
|
&--primary {
|
||||||
background: var(--projects-button-primary-bg);
|
background: var(--projects-button-primary-bg);
|
||||||
|
|
@ -139,6 +209,15 @@
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 2px solid var(--color-focus-ring, #2563eb);
|
||||||
|
outline-offset: 2px;
|
||||||
|
z-index: 10;
|
||||||
|
transform: scale(1.05);
|
||||||
|
// Ensure overlay stays visible when button is focused
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__action-icon {
|
&__action-icon {
|
||||||
|
|
@ -185,6 +264,83 @@
|
||||||
border: 1px solid var(--projects-tech-badge-border);
|
border: 1px solid var(--projects-tech-badge-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__help {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__help-details {
|
||||||
|
display: inline-block;
|
||||||
|
background: var(--projects-card-background);
|
||||||
|
border: 1px solid var(--projects-card-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__help-summary {
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--projects-button-secondary-text);
|
||||||
|
list-style: none; // Remove default marker
|
||||||
|
|
||||||
|
&::-webkit-details-marker {
|
||||||
|
display: none; // Remove default marker in webkit browsers
|
||||||
|
}
|
||||||
|
|
||||||
|
&::marker {
|
||||||
|
display: none; // Remove default marker in other browsers
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 2px solid var(--color-focus-ring, #2563eb);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: ' ▼';
|
||||||
|
font-size: 0.75rem;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__help-details[open] &__help-summary::after {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__help-content {
|
||||||
|
margin-top: 1rem;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--projects-button-secondary-text);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
background: var(--projects-tech-badge-bg);
|
||||||
|
color: var(--projects-tech-badge-text);
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
border: 1px solid var(--projects-tech-badge-border);
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Responsive adjustments
|
// Responsive adjustments
|
||||||
@include globals.mobile-only {
|
@include globals.mobile-only {
|
||||||
&__card {
|
&__card {
|
||||||
|
|
@ -202,11 +358,222 @@
|
||||||
&__actions {
|
&__actions {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
padding: 1rem;
|
||||||
|
|
||||||
|
// Adjust transform for mobile
|
||||||
|
transform: translate(-50%, -50%) translateY(15px);
|
||||||
|
|
||||||
|
&--visible {
|
||||||
|
transform: translate(-50%, -50%) translateY(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__action-button {
|
&__action-button {
|
||||||
justify-content: center;
|
min-width: 140px; // Slightly wider on mobile for touch targets
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__help-details {
|
||||||
|
max-width: 90%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__help-content {
|
||||||
|
li {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 0.15rem 0.3rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Screen reader only classes
|
||||||
|
.sr-only {
|
||||||
|
position: absolute !important;
|
||||||
|
width: 1px !important;
|
||||||
|
height: 1px !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: -1px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
clip: rect(0, 0, 0, 0) !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
border: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Card states
|
||||||
|
.projects-section__card {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 3px solid var(--color-focus-ring, #2563eb);
|
||||||
|
outline-offset: 2px;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--active {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: var(--projects-card-shadow-hover);
|
||||||
|
border-color: var(--color-focus-ring, #2563eb);
|
||||||
|
border-width: 2px;
|
||||||
|
|
||||||
|
.projects-section__actions {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action buttons visibility
|
||||||
|
.projects-section__actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%) translateY(10px);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
z-index: 10; // Above overlay
|
||||||
|
|
||||||
|
&--visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) translateY(0);
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override for when actions are shown (hover, focus, active)
|
||||||
|
.projects-section__card {
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&--active,
|
||||||
|
&:focus-within {
|
||||||
|
.projects-section__actions {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) translateY(0);
|
||||||
|
pointer-events: all;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// High contrast mode
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
.projects-section__card {
|
||||||
|
&:focus,
|
||||||
|
&--active {
|
||||||
|
outline: 4px solid currentColor !important;
|
||||||
|
background: var(--projects-card-background) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-section__action-button {
|
||||||
|
border: 2px solid currentColor !important;
|
||||||
|
background: var(--projects-card-background) !important;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 3px solid currentColor !important;
|
||||||
|
background: var(--color-focus-ring, #2563eb) !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.projects-section {
|
||||||
|
&__overlay {
|
||||||
|
background: rgba(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0.8
|
||||||
|
) !important; // Stronger overlay for high contrast
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
background: var(--projects-card-background) !important;
|
||||||
|
border: 2px solid currentColor !important;
|
||||||
|
backdrop-filter: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__help-details {
|
||||||
|
border: 2px solid currentColor !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__help-summary {
|
||||||
|
border: 1px solid transparent;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
border-color: currentColor !important;
|
||||||
|
outline: 3px solid currentColor !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
border: 1px solid currentColor !important;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark theme adjustments for help section:
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.projects-section {
|
||||||
|
&__actions {
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@include globals.mobile-only {
|
||||||
|
.projects-section {
|
||||||
|
&__actions {
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduced motion updates to include help section:
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.projects-section {
|
||||||
|
&__help-summary::after {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__actions {
|
||||||
|
transition: none !important;
|
||||||
|
backdrop-filter: none !important; // Remove blur effect for reduced motion
|
||||||
|
|
||||||
|
&--visible {
|
||||||
|
transform: translate(-50%, -50%) translateY(0) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__card {
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&--active,
|
||||||
|
&:focus-within {
|
||||||
|
.projects-section__actions {
|
||||||
|
transform: translate(-50%, -50%) translateY(0) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus ring color custom property
|
||||||
|
:root {
|
||||||
|
--color-focus-ring: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--color-focus-ring: #60a5fa;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,7 @@
|
||||||
font-size: 1.875rem;
|
font-size: 1.875rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
background: var(--gradient-services-title);
|
color: var(--color-text);
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
|
|
||||||
@include globals.desktop-only {
|
@include globals.desktop-only {
|
||||||
font-size: 2.25rem;
|
font-size: 2.25rem;
|
||||||
|
|
@ -100,17 +96,40 @@
|
||||||
&__card {
|
&__card {
|
||||||
background: white;
|
background: white;
|
||||||
border-radius: $card-border-radius-md;
|
border-radius: $card-border-radius-md;
|
||||||
box-shadow: $shadow-card;
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||||
transition: $transition-base;
|
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: auto;
|
height: auto;
|
||||||
min-height: 250px; // Ensures consistent card heights
|
min-height: 250px; // Ensures consistent card heights
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
|
||||||
|
// Add subtle backdrop blur for depth
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: inherit;
|
||||||
|
backdrop-filter: blur(1px);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: translateY(-4px);
|
transform: translateY(-8px) scale(1.02);
|
||||||
box-shadow: $shadow-card-hover;
|
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25),
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.3);
|
||||||
|
filter: brightness(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
outline: 3px solid var(--color-focus);
|
||||||
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Responsive card heights - more flexible approach
|
// Responsive card heights - more flexible approach
|
||||||
|
|
@ -171,6 +190,8 @@
|
||||||
height: $services-icon-size;
|
height: $services-icon-size;
|
||||||
margin: 0 auto 1rem auto;
|
margin: 0 auto 1rem auto;
|
||||||
display: block;
|
display: block;
|
||||||
|
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
|
|
@ -178,6 +199,12 @@
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add subtle hover animation for icons
|
||||||
|
.services-section__card:hover & {
|
||||||
|
transform: scale(1.1) rotate(5deg);
|
||||||
|
filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.2));
|
||||||
|
}
|
||||||
|
|
||||||
&--primary {
|
&--primary {
|
||||||
color: var(--service-icon-primary);
|
color: var(--service-icon-primary);
|
||||||
}
|
}
|
||||||
|
|
@ -208,6 +235,8 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||||
|
letter-spacing: 0.025em;
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
|
@ -259,6 +288,8 @@
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
|
|
@ -340,3 +371,144 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// WCAG Level AAA Accessibility Enhancements
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Enhanced focus states for keyboard navigation
|
||||||
|
.services-section__card {
|
||||||
|
// Make cards keyboard accessible
|
||||||
|
&[tabindex='0'] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// AAA-compliant focus indicator (3px minimum)
|
||||||
|
&:focus {
|
||||||
|
outline: 3px solid var(--color-focus-ring, #2563eb);
|
||||||
|
outline-offset: 2px;
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhanced hover and focus interaction
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: $shadow-card-hover;
|
||||||
|
|
||||||
|
// When both focused and hovered
|
||||||
|
&:focus {
|
||||||
|
outline: 3px solid var(--color-focus-ring, #2563eb);
|
||||||
|
outline-offset: 2px;
|
||||||
|
transform: translateY(-6px); // Slightly more lift when focused
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// High contrast mode support for better accessibility
|
||||||
|
@media (prefers-contrast: high) {
|
||||||
|
.services-section__card {
|
||||||
|
border: 2px solid currentColor !important;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: 4px solid currentColor !important;
|
||||||
|
outline-offset: 3px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enhance icon visibility in high contrast
|
||||||
|
.services-section__icon {
|
||||||
|
filter: contrast(1.5) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure text remains readable in high contrast mode
|
||||||
|
.services-section__card-title,
|
||||||
|
.services-section__card-description {
|
||||||
|
text-shadow: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reduced motion support
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.services-section__card {
|
||||||
|
transition: none !important;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
transform: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Touch device optimizations for AAA compliance
|
||||||
|
@media (hover: none) and (pointer: coarse) {
|
||||||
|
.services-section__card {
|
||||||
|
// Ensure minimum 44x44px touch targets (cards are already large enough)
|
||||||
|
min-height: 320px;
|
||||||
|
|
||||||
|
// Remove hover effects for true touch devices
|
||||||
|
&:hover {
|
||||||
|
transform: none;
|
||||||
|
box-shadow: $shadow-card;
|
||||||
|
}
|
||||||
|
|
||||||
|
// But maintain focus for keyboard users on touch devices
|
||||||
|
&:focus {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: $shadow-card-hover;
|
||||||
|
outline: 3px solid var(--color-focus-ring, #2563eb);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark theme specific enhancements for better visibility
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.services-section__card {
|
||||||
|
// Enhance card visibility with better shadows in dark mode
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3),
|
||||||
|
0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.4),
|
||||||
|
0 10px 10px -5px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subtle loading animation for cards
|
||||||
|
@keyframes cardFadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Animation delays for staggered effect
|
||||||
|
.services-section__card {
|
||||||
|
animation: cardFadeIn 0.6s ease-out;
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
animation-delay: 0.1s;
|
||||||
|
}
|
||||||
|
&:nth-child(2) {
|
||||||
|
animation-delay: 0.2s;
|
||||||
|
}
|
||||||
|
&:nth-child(3) {
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
&:nth-child(4) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
&:nth-child(5) {
|
||||||
|
animation-delay: 0.5s;
|
||||||
|
}
|
||||||
|
&:nth-child(6) {
|
||||||
|
animation-delay: 0.6s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,11 +49,7 @@
|
||||||
font-size: 1.875rem;
|
font-size: 1.875rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
background: var(--gradient-skills-title);
|
color: var(--color-text);
|
||||||
background-clip: text;
|
|
||||||
-webkit-background-clip: text;
|
|
||||||
color: transparent;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
|
|
||||||
@include globals.desktop-only {
|
@include globals.desktop-only {
|
||||||
font-size: 2.25rem;
|
font-size: 2.25rem;
|
||||||
|
|
@ -261,17 +257,28 @@
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__skill-item {
|
||||||
|
// Individual skill container styling with consistent height
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 4rem; // Ensure consistent height for all skill items
|
||||||
|
}
|
||||||
|
|
||||||
&__skill-header {
|
&__skill-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: flex-start; // Changed from center to flex-start to handle wrapping
|
||||||
margin-bottom: 0.5rem;
|
margin-bottom: 0.5rem;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
min-height: 2.5rem; // Reserve space for wrapped text
|
||||||
|
flex: 0 0 auto; // Don't grow or shrink
|
||||||
|
|
||||||
@include globals.mobile-only {
|
@include globals.mobile-only {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
|
min-height: 3rem; // More space on mobile for wrapped text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -279,6 +286,9 @@
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--skills-skill-name-color);
|
color: var(--skills-skill-name-color);
|
||||||
|
line-height: 1.3; // Better line height for wrapped text
|
||||||
|
flex: 1; // Take available space
|
||||||
|
min-width: 0; // Allow text to wrap properly
|
||||||
|
|
||||||
@include globals.mobile-only {
|
@include globals.mobile-only {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|
@ -289,6 +299,8 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
|
flex-shrink: 0; // Prevent shrinking
|
||||||
|
align-self: flex-start; // Align to top when skill name wraps
|
||||||
|
|
||||||
@include globals.mobile-only {
|
@include globals.mobile-only {
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
|
|
@ -320,6 +332,12 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__progress-container {
|
||||||
|
// Ensure progress bar is always at the bottom
|
||||||
|
margin-top: auto;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
&__progress {
|
&__progress {
|
||||||
height: $skills-progress-height;
|
height: $skills-progress-height;
|
||||||
background: var(--skills-progress-bg);
|
background: var(--skills-progress-bg);
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ $base-light-theme: (
|
||||||
active-box-shadow: rgba(0, 0, 0, 0.6),
|
active-box-shadow: rgba(0, 0, 0, 0.6),
|
||||||
hover-box-shadow: rgba(0, 0, 0, 0.9),
|
hover-box-shadow: rgba(0, 0, 0, 0.9),
|
||||||
gradient-primary: $gradient-primary,
|
gradient-primary: $gradient-primary,
|
||||||
gradient-text: $gradient-text,
|
gradient-text: $gradient-text-light,
|
||||||
|
|
||||||
// Shadow variables
|
// Shadow variables
|
||||||
box-shadow-sm: #{$shadow-sm},
|
box-shadow-sm: #{$shadow-sm},
|
||||||
|
|
@ -59,12 +59,12 @@ $base-light-theme: (
|
||||||
$base-dark-theme: (
|
$base-dark-theme: (
|
||||||
primary: #9333ea,
|
primary: #9333ea,
|
||||||
background: #1f2937,
|
background: #1f2937,
|
||||||
text: #f9fafb,
|
text: #ffffff,
|
||||||
text-muted: #9ca3af,
|
text-muted: #ffffff,
|
||||||
active-box-shadow: rgba(255, 255, 255, 0.1),
|
active-box-shadow: rgba(255, 255, 255, 0.1),
|
||||||
hover-box-shadow: rgba(255, 255, 255, 0.15),
|
hover-box-shadow: rgba(255, 255, 255, 0.15),
|
||||||
gradient-primary: $gradient-primary,
|
gradient-primary: $gradient-primary,
|
||||||
gradient-text: $gradient-text,
|
gradient-text: $gradient-text-dark,
|
||||||
|
|
||||||
// Shadow variables
|
// Shadow variables
|
||||||
box-shadow-sm: #{$shadow-sm},
|
box-shadow-sm: #{$shadow-sm},
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,16 @@ $red-light: #f87171;
|
||||||
$red-lighter: #fca5a5;
|
$red-lighter: #fca5a5;
|
||||||
$red-lightest: #fee2e2;
|
$red-lightest: #fee2e2;
|
||||||
|
|
||||||
|
// Neutral gray colors - extracted from hardcoded values
|
||||||
|
$gray-neutral-dark: #374151; // Contact input bg dark theme
|
||||||
|
$gray-neutral-medium: #1f2937; // Social bg dark theme
|
||||||
|
$gray-neutral-light: #6b7280; // Text muted light theme
|
||||||
|
$gray-neutral-lighter: #9ca3af; // Input placeholder dark theme
|
||||||
|
$gray-neutral-lightest: #d1d5db; // Text muted dark theme
|
||||||
|
|
||||||
|
// Text colors - extracted from hardcoded values
|
||||||
|
$text-input-placeholder-light: #484c56; // Input placeholder light theme
|
||||||
|
|
||||||
$white: #ffffff;
|
$white: #ffffff;
|
||||||
$black: #000000;
|
$black: #000000;
|
||||||
|
|
||||||
|
|
@ -86,7 +96,7 @@ $contact-light-theme: (
|
||||||
'contact-input-focus-ring': rgba($orange-primary, $alpha-border-light),
|
'contact-input-focus-ring': rgba($orange-primary, $alpha-border-light),
|
||||||
'contact-input-text': $text-aaa-light,
|
'contact-input-text': $text-aaa-light,
|
||||||
// Added for AAA compliance
|
// Added for AAA compliance
|
||||||
'contact-input-placeholder': #484c56,
|
'contact-input-placeholder': $text-input-placeholder-light,
|
||||||
|
|
||||||
// Added for form accessibility
|
// Added for form accessibility
|
||||||
// Button styling - improved contrast and readability
|
// Button styling - improved contrast and readability
|
||||||
|
|
@ -110,7 +120,27 @@ $contact-light-theme: (
|
||||||
'contact-status-success-text': $green-secondary,
|
'contact-status-success-text': $green-secondary,
|
||||||
'contact-status-error-bg': $red-lightest,
|
'contact-status-error-bg': $red-lightest,
|
||||||
'contact-status-error-border': $red-primary,
|
'contact-status-error-border': $red-primary,
|
||||||
'contact-status-error-text': $red-secondary
|
'contact-status-error-text': $red-secondary,
|
||||||
|
|
||||||
|
// Text colors for AAA compliance
|
||||||
|
'color-text': $text-aaa-light,
|
||||||
|
// Near black for AAA contrast
|
||||||
|
'color-text-muted': $gray-neutral-light,
|
||||||
|
// Gray for secondary text
|
||||||
|
'primary-color': $indigo-primary,
|
||||||
|
// Indigo primary
|
||||||
|
'text-primary': $text-aaa-light,
|
||||||
|
// Main text color
|
||||||
|
'text-secondary': $gray-neutral-light,
|
||||||
|
|
||||||
|
// Secondary text color
|
||||||
|
// Enhanced social button colors for AAA compliance
|
||||||
|
'social-icon-email': $text-aaa-light,
|
||||||
|
// Dark for better contrast
|
||||||
|
'social-icon-github': $text-aaa-light,
|
||||||
|
// Dark for better contrast
|
||||||
|
'social-icon-linkedin': $text-aaa-light,
|
||||||
|
// Dark for better contrast
|
||||||
);
|
);
|
||||||
|
|
||||||
$contact-dark-theme: (
|
$contact-dark-theme: (
|
||||||
|
|
@ -134,13 +164,13 @@ $contact-dark-theme: (
|
||||||
'contact-form-shadow': $shadow-dark,
|
'contact-form-shadow': $shadow-dark,
|
||||||
|
|
||||||
// Input styling (dark theme) - FIXED for AAA contrast
|
// Input styling (dark theme) - FIXED for AAA contrast
|
||||||
'contact-input-bg': #374151,
|
'contact-input-bg': $gray-neutral-dark,
|
||||||
// Changed to neutral dark gray
|
// Changed to neutral dark gray
|
||||||
'contact-input-border': rgba($orange-primary, $alpha-border-heavy),
|
'contact-input-border': rgba($orange-primary, $alpha-border-heavy),
|
||||||
'contact-input-focus-ring': rgba($orange-primary, $alpha-border-medium),
|
'contact-input-focus-ring': rgba($orange-primary, $alpha-border-medium),
|
||||||
'contact-input-text': $text-aaa-dark,
|
'contact-input-text': $text-aaa-dark,
|
||||||
// White text for AAA contrast
|
// White text for AAA contrast
|
||||||
'contact-input-placeholder': #9ca3af,
|
'contact-input-placeholder': $gray-neutral-lighter,
|
||||||
|
|
||||||
// Light gray for placeholder
|
// Light gray for placeholder
|
||||||
// Button styling (dark theme) - improved contrast
|
// Button styling (dark theme) - improved contrast
|
||||||
|
|
@ -150,12 +180,12 @@ $contact-dark-theme: (
|
||||||
'contact-button-hover-bg':
|
'contact-button-hover-bg':
|
||||||
linear-gradient(135deg, $blue-lighter 0%, $blue-light 100%),
|
linear-gradient(135deg, $blue-lighter 0%, $blue-light 100%),
|
||||||
// Social button styling (dark theme) - FIXED for AAA contrast
|
// Social button styling (dark theme) - FIXED for AAA contrast
|
||||||
'contact-social-bg': #1f2937,
|
'contact-social-bg': $gray-neutral-medium,
|
||||||
// Changed to neutral dark background
|
// Changed to neutral dark background
|
||||||
'contact-social-text': $text-aaa-dark,
|
'contact-social-text': $text-aaa-dark,
|
||||||
// White text for AAA contrast
|
// White text for AAA contrast
|
||||||
'contact-social-border': $orange-secondary,
|
'contact-social-border': $orange-secondary,
|
||||||
'contact-social-hover-bg': #374151,
|
'contact-social-hover-bg': $gray-neutral-dark,
|
||||||
// Slightly lighter on hover
|
// Slightly lighter on hover
|
||||||
'contact-social-hover-border': $orange-primary,
|
'contact-social-hover-border': $orange-primary,
|
||||||
|
|
||||||
|
|
@ -165,5 +195,25 @@ $contact-dark-theme: (
|
||||||
'contact-status-success-text': $green-lighter,
|
'contact-status-success-text': $green-lighter,
|
||||||
'contact-status-error-bg': rgba($red-light, $alpha-low),
|
'contact-status-error-bg': rgba($red-light, $alpha-low),
|
||||||
'contact-status-error-border': $red-light,
|
'contact-status-error-border': $red-light,
|
||||||
'contact-status-error-text': $red-lighter
|
'contact-status-error-text': $red-lighter,
|
||||||
|
|
||||||
|
// Text colors for AAA compliance - ALL WHITE in dark mode
|
||||||
|
'color-text': $text-aaa-dark,
|
||||||
|
// White for AAA contrast on dark
|
||||||
|
'color-text-muted': $text-aaa-dark,
|
||||||
|
// Changed to white for consistency
|
||||||
|
'primary-color': $indigo-light,
|
||||||
|
// Lighter indigo for dark mode
|
||||||
|
'text-primary': $text-aaa-dark,
|
||||||
|
// Main text color
|
||||||
|
'text-secondary': $text-aaa-dark,
|
||||||
|
// Changed to white for consistency
|
||||||
|
// Secondary text color
|
||||||
|
// Enhanced social button colors for AAA compliance
|
||||||
|
'social-icon-email': $text-aaa-dark,
|
||||||
|
// White for better contrast
|
||||||
|
'social-icon-github': $text-aaa-dark,
|
||||||
|
// White for better contrast
|
||||||
|
'social-icon-linkedin': $text-aaa-dark,
|
||||||
|
// White for better contrast
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,16 @@
|
||||||
// Color Variables - Change once, use everywhere
|
// Color Variables - Change once, use everywhere
|
||||||
// ================================
|
// ================================
|
||||||
|
|
||||||
// Light theme colors
|
// Light theme colors - Darker for AAA compliance against colorful backgrounds
|
||||||
$light-primary-color: #0f172a; // Much darker blue (was #18338c)
|
$light-primary-color: #0c4a6e; // Dark blue for blue gradient
|
||||||
$light-secondary-color: #0f2419; // Much darker green (was #13582e)
|
$light-secondary-color: #064e3b; // Dark green for green gradient
|
||||||
$light-tertiary-color: #2d1b69; // Much darker purple (was #551fad)
|
$light-tertiary-color: #581c87; // Dark purple for purple gradient
|
||||||
$light-quaternary-color: #042f2e; // Much darker teal (was #073c39)
|
$light-quaternary-color: #134e4a; // Dark teal for teal gradient
|
||||||
$light-quinary-color: #451a01; // Much darker orange (was #6f2406)
|
$light-quinary-color: #9a3412; // Dark orange for orange gradient
|
||||||
$light-senary-color: #7f1d1d; // Much darker red (was #b91c1c)
|
$light-senary-color: #991b1b; // Dark red for red gradient
|
||||||
|
|
||||||
// Alternative: Use neutral dark colors for guaranteed AAA compliance
|
// Alternative: Use universal dark colors for guaranteed AAA compliance
|
||||||
$light-text-primary: #111827; // Near black
|
$light-text-primary: #111827; // Near black - universal fallback
|
||||||
$light-text-secondary: #1f2937; // Dark gray
|
$light-text-secondary: #1f2937; // Dark gray
|
||||||
$light-text-tertiary: #374151; // Medium dark gray
|
$light-text-tertiary: #374151; // Medium dark gray
|
||||||
|
|
||||||
|
|
@ -30,57 +30,118 @@ $dark-quaternary-title: #5eead4;
|
||||||
$dark-quinary-title: #fdba74;
|
$dark-quinary-title: #fdba74;
|
||||||
$dark-senary-title: #fca5a5;
|
$dark-senary-title: #fca5a5;
|
||||||
|
|
||||||
$dark-description-color: #ffffff; // Pure white instead of #f1f5f9 // Universal description color for dark theme
|
// Background gradients - Enhanced for visual appeal
|
||||||
|
$light-services-bg: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
#f8fafc 0%,
|
||||||
|
#e2e8f0 50%,
|
||||||
|
#cbd5e1 100%
|
||||||
|
);
|
||||||
|
$dark-services-bg: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
#0f172a 0%,
|
||||||
|
#1e293b 50%,
|
||||||
|
#334155 100%
|
||||||
|
);
|
||||||
|
|
||||||
// Background gradients
|
$light-title-gradient: linear-gradient(90deg, #0c4a6e, #1e40af, #3b82f6);
|
||||||
$light-services-bg: linear-gradient(135deg, #e0e7ef 0%, #91c5e0 100%);
|
$dark-title-gradient: linear-gradient(90deg, #60a5fa, #3b82f6, #2563eb);
|
||||||
$dark-services-bg: linear-gradient(135deg, #78350f 0%, #a78bfa 100%);
|
|
||||||
|
|
||||||
$light-title-gradient: linear-gradient(90deg, #0f172a, #1e293b, #334155);
|
// Card background gradients - Vibrant and diverse with AAA compliance
|
||||||
$dark-title-gradient: linear-gradient(90deg, #f1f5f9, #e2e8f0, #cbd5e1);
|
|
||||||
|
|
||||||
// Card background gradients
|
|
||||||
$light-card-primary-bg: linear-gradient(
|
$light-card-primary-bg: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
#f8fafc,
|
#dbeafe 0%,
|
||||||
#f1f5f9
|
#bfdbfe 50%,
|
||||||
); // Lighter backgrounds
|
#93c5fd 100%
|
||||||
$light-card-secondary-bg: linear-gradient(135deg, #f9fafb, #f3f4f6);
|
); // Ocean Blue gradient
|
||||||
$light-card-tertiary-bg: linear-gradient(135deg, #faf9ff, #f5f3ff);
|
$light-card-secondary-bg: linear-gradient(
|
||||||
$light-card-quaternary-bg: linear-gradient(135deg, #f0fdfa, #ecfdf5);
|
135deg,
|
||||||
$light-card-quinary-bg: linear-gradient(135deg, #fffbeb, #fef3c7);
|
#d1fae5 0%,
|
||||||
$light-card-senary-bg: linear-gradient(135deg, #fef2f2, #fee2e2);
|
#a7f3d0 50%,
|
||||||
|
#6ee7b7 100%
|
||||||
|
); // Emerald Green gradient
|
||||||
|
$light-card-tertiary-bg: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
#e9d5ff 0%,
|
||||||
|
#ddd6fe 50%,
|
||||||
|
#c4b5fd 100%
|
||||||
|
); // Purple Violet gradient
|
||||||
|
$light-card-quaternary-bg: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
#ccfbf1 0%,
|
||||||
|
#99f6e4 50%,
|
||||||
|
#5eead4 100%
|
||||||
|
); // Teal Cyan gradient
|
||||||
|
$light-card-quinary-bg: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
#fed7aa 0%,
|
||||||
|
#fdba74 50%,
|
||||||
|
#fb923c 100%
|
||||||
|
); // Warm Orange gradient
|
||||||
|
$light-card-senary-bg: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
#fecaca 0%,
|
||||||
|
#fca5a5 50%,
|
||||||
|
#f87171 100%
|
||||||
|
); // Coral Red gradient
|
||||||
|
|
||||||
|
// Dark theme card gradients - Rich and atmospheric
|
||||||
$dark-card-primary-bg: linear-gradient(
|
$dark-card-primary-bg: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
rgba(30, 58, 138, 0.6),
|
rgba(30, 58, 138, 0.8) 0%,
|
||||||
rgba(37, 99, 235, 0.5)
|
rgba(37, 99, 235, 0.7) 50%,
|
||||||
);
|
rgba(59, 130, 246, 0.6) 100%
|
||||||
|
); // Deep Ocean Blue
|
||||||
$dark-card-secondary-bg: linear-gradient(
|
$dark-card-secondary-bg: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
rgba(22, 101, 52, 0.6),
|
rgba(22, 101, 52, 0.8) 0%,
|
||||||
rgba(34, 197, 94, 0.5)
|
rgba(34, 197, 94, 0.7) 50%,
|
||||||
);
|
rgba(74, 222, 128, 0.6) 100%
|
||||||
|
); // Forest Emerald
|
||||||
$dark-card-tertiary-bg: linear-gradient(
|
$dark-card-tertiary-bg: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
rgba(91, 33, 182, 0.6),
|
rgba(91, 33, 182, 0.8) 0%,
|
||||||
rgba(124, 58, 237, 0.5)
|
rgba(124, 58, 237, 0.7) 50%,
|
||||||
);
|
rgba(147, 51, 234, 0.6) 100%
|
||||||
|
); // Royal Purple
|
||||||
$dark-card-quaternary-bg: linear-gradient(
|
$dark-card-quaternary-bg: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
rgba(17, 94, 89, 0.6),
|
rgba(17, 94, 89, 0.8) 0%,
|
||||||
rgba(20, 184, 166, 0.5)
|
rgba(20, 184, 166, 0.7) 50%,
|
||||||
);
|
rgba(45, 212, 191, 0.6) 100%
|
||||||
|
); // Mystic Teal
|
||||||
$dark-card-quinary-bg: linear-gradient(
|
$dark-card-quinary-bg: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
rgba(154, 52, 18, 0.6),
|
rgba(154, 52, 18, 0.8) 0%,
|
||||||
rgba(249, 115, 22, 0.5)
|
rgba(249, 115, 22, 0.7) 50%,
|
||||||
);
|
rgba(251, 146, 60, 0.6) 100%
|
||||||
|
); // Sunset Orange
|
||||||
$dark-card-senary-bg: linear-gradient(
|
$dark-card-senary-bg: linear-gradient(
|
||||||
135deg,
|
135deg,
|
||||||
rgba(153, 27, 27, 0.6),
|
rgba(153, 27, 27, 0.8) 0%,
|
||||||
rgba(239, 68, 68, 0.5)
|
rgba(239, 68, 68, 0.7) 50%,
|
||||||
);
|
rgba(248, 113, 113, 0.6) 100%
|
||||||
|
); // Crimson Fire
|
||||||
|
|
||||||
|
// ================================
|
||||||
|
// Additional variables needed for theme maps
|
||||||
|
// ================================
|
||||||
|
|
||||||
|
// Dark theme icon colors (brighter for better contrast)
|
||||||
|
$dark-primary-icon: #60a5fa; // Light blue
|
||||||
|
$dark-secondary-icon: #34d399; // Light green
|
||||||
|
$dark-tertiary-icon: #a78bfa; // Light purple
|
||||||
|
$dark-quaternary-icon: #5eead4; // Light teal
|
||||||
|
$dark-quinary-icon: #fbbf24; // Light orange
|
||||||
|
$dark-senary-icon: #f87171; // Light red
|
||||||
|
|
||||||
|
// Dark theme title colors (medium contrast)
|
||||||
|
$dark-primary-title: #93c5fd; // Medium blue
|
||||||
|
$dark-secondary-title: #6ee7b7; // Medium green
|
||||||
|
$dark-tertiary-title: #c4b5fd; // Medium purple
|
||||||
|
$dark-quaternary-title: #7dd3fc; // Medium teal
|
||||||
|
$dark-quinary-title: #fed7aa; // Medium orange
|
||||||
|
$dark-senary-title: #fca5a5; // Medium red
|
||||||
|
|
||||||
// ================================
|
// ================================
|
||||||
// Theme Maps - Using variables above
|
// Theme Maps - Using variables above
|
||||||
|
|
@ -100,28 +161,28 @@ $services-light-theme: (
|
||||||
service-card-senary-bg: $light-card-senary-bg,
|
service-card-senary-bg: $light-card-senary-bg,
|
||||||
|
|
||||||
// Icons - using same color for consistency
|
// Icons - using same color for consistency
|
||||||
service-icon-primary: $light-primary-color,
|
service-icon-primary: $light-text-primary,
|
||||||
service-icon-secondary: $light-secondary-color,
|
service-icon-secondary: $light-text-primary,
|
||||||
service-icon-tertiary: $light-tertiary-color,
|
service-icon-tertiary: $light-text-primary,
|
||||||
service-icon-quaternary: $light-quaternary-color,
|
service-icon-quaternary: $light-text-primary,
|
||||||
service-icon-quinary: $light-quinary-color,
|
service-icon-quinary: $light-text-primary,
|
||||||
service-icon-senary: $light-senary-color,
|
service-icon-senary: $light-text-primary,
|
||||||
|
|
||||||
// Titles - using same color as icons
|
// Titles - using same color as icons
|
||||||
service-title-primary: $light-primary-color,
|
service-title-primary: $light-text-primary,
|
||||||
service-title-secondary: $light-secondary-color,
|
service-title-secondary: $light-text-primary,
|
||||||
service-title-tertiary: $light-tertiary-color,
|
service-title-tertiary: $light-text-primary,
|
||||||
service-title-quaternary: $light-quaternary-color,
|
service-title-quaternary: $light-text-primary,
|
||||||
service-title-quinary: $light-quinary-color,
|
service-title-quinary: $light-text-primary,
|
||||||
service-title-senary: $light-senary-color,
|
service-title-senary: $light-text-primary,
|
||||||
|
|
||||||
// Descriptions - using same color as icons and titles
|
// Descriptions - using same color as icons and titles
|
||||||
service-description-primary: $light-primary-color,
|
service-description-primary: $light-text-primary,
|
||||||
service-description-secondary: $light-secondary-color,
|
service-description-secondary: $light-text-primary,
|
||||||
service-description-tertiary: $light-tertiary-color,
|
service-description-tertiary: $light-text-primary,
|
||||||
service-description-quaternary: $light-quaternary-color,
|
service-description-quaternary: $light-text-primary,
|
||||||
service-description-quinary: $light-quinary-color,
|
service-description-quinary: $light-text-primary,
|
||||||
service-description-senary: $light-senary-color
|
service-description-senary: $light-text-primary
|
||||||
);
|
);
|
||||||
|
|
||||||
$services-dark-theme: (
|
$services-dark-theme: (
|
||||||
|
|
@ -137,27 +198,27 @@ $services-dark-theme: (
|
||||||
service-card-quinary-bg: $dark-card-quinary-bg,
|
service-card-quinary-bg: $dark-card-quinary-bg,
|
||||||
service-card-senary-bg: $dark-card-senary-bg,
|
service-card-senary-bg: $dark-card-senary-bg,
|
||||||
|
|
||||||
// Icons - using distinct colors for better contrast
|
// Icons - all using pure white
|
||||||
service-icon-primary: $dark-primary-icon,
|
service-icon-primary: #ffffff,
|
||||||
service-icon-secondary: $dark-secondary-icon,
|
service-icon-secondary: #ffffff,
|
||||||
service-icon-tertiary: $dark-tertiary-icon,
|
service-icon-tertiary: #ffffff,
|
||||||
service-icon-quaternary: $dark-quaternary-icon,
|
service-icon-quaternary: #ffffff,
|
||||||
service-icon-quinary: $dark-quinary-icon,
|
service-icon-quinary: #ffffff,
|
||||||
service-icon-senary: $dark-senary-icon,
|
service-icon-senary: #ffffff,
|
||||||
|
|
||||||
// Titles - using medium contrast colors
|
// Titles - all using pure white
|
||||||
service-title-primary: $dark-primary-title,
|
service-title-primary: #ffffff,
|
||||||
service-title-secondary: $dark-secondary-title,
|
service-title-secondary: #ffffff,
|
||||||
service-title-tertiary: $dark-tertiary-title,
|
service-title-tertiary: #ffffff,
|
||||||
service-title-quaternary: $dark-quaternary-title,
|
service-title-quaternary: #ffffff,
|
||||||
service-title-quinary: $dark-quinary-title,
|
service-title-quinary: #ffffff,
|
||||||
service-title-senary: $dark-senary-title,
|
service-title-senary: #ffffff,
|
||||||
|
|
||||||
// Descriptions - all using the same high-contrast color
|
// Descriptions - all using pure white
|
||||||
service-description-primary: $dark-description-color,
|
service-description-primary: #ffffff,
|
||||||
service-description-secondary: $dark-description-color,
|
service-description-secondary: #ffffff,
|
||||||
service-description-tertiary: $dark-description-color,
|
service-description-tertiary: #ffffff,
|
||||||
service-description-quaternary: $dark-description-color,
|
service-description-quaternary: #ffffff,
|
||||||
service-description-quinary: $dark-description-color,
|
service-description-quinary: #ffffff,
|
||||||
service-description-senary: $dark-description-color
|
service-description-senary: #ffffff
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -184,10 +184,10 @@ $skills-dark-theme: (
|
||||||
linear-gradient(90deg, $blue-light, $green-light, $purple-light),
|
linear-gradient(90deg, $blue-light, $green-light, $purple-light),
|
||||||
skills-category-bg: rgba($slate-800, $alpha-bg-card),
|
skills-category-bg: rgba($slate-800, $alpha-bg-card),
|
||||||
skills-category-border: rgba($slate-600, $alpha-heavy),
|
skills-category-border: rgba($slate-600, $alpha-heavy),
|
||||||
skills-category-title-color: $slate-100,
|
skills-category-title-color: #ffffff,
|
||||||
skills-skill-name-color: $slate-300,
|
skills-skill-name-color: #ffffff,
|
||||||
skills-skill-level-color: $slate-400,
|
skills-skill-level-color: #ffffff,
|
||||||
skills-skill-percentage-color: $slate-200,
|
skills-skill-percentage-color: #ffffff,
|
||||||
skills-progress-bg: rgba($slate-700, $alpha-bg-card),
|
skills-progress-bg: rgba($slate-700, $alpha-bg-card),
|
||||||
// Dark theme card backgrounds
|
// Dark theme card backgrounds
|
||||||
skills-category-bg-primary:
|
skills-category-bg-primary:
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ $border-radius-sm: 0.375rem;
|
||||||
$border-radius-md: 0.5rem;
|
$border-radius-md: 0.5rem;
|
||||||
$border-radius-lg: 1rem;
|
$border-radius-lg: 1rem;
|
||||||
$border-radius-xl: 1.5rem;
|
$border-radius-xl: 1.5rem;
|
||||||
$border-radius-full: 9999px;
|
|
||||||
|
|
||||||
// Spacing variables
|
// Spacing variables
|
||||||
$spacing-xs: 0.5rem;
|
$spacing-xs: 0.5rem;
|
||||||
|
|
@ -64,13 +63,21 @@ $gradient-primary: linear-gradient(
|
||||||
$color-secondary 0%,
|
$color-secondary 0%,
|
||||||
$color-primary 100%
|
$color-primary 100%
|
||||||
);
|
);
|
||||||
$gradient-text: linear-gradient(
|
|
||||||
|
// Light theme gradient (keep original)
|
||||||
|
$gradient-text-light: linear-gradient(
|
||||||
to right,
|
to right,
|
||||||
$color-secondary,
|
$color-secondary,
|
||||||
$color-primary,
|
$color-primary,
|
||||||
#0d9488
|
#0d9488
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Dark theme gradient (white to light purple for hero title)
|
||||||
|
$gradient-text-dark: linear-gradient(to right, #ffffff, #c4b5fd, #a78bfa);
|
||||||
|
|
||||||
|
// Default gradient for backwards compatibility
|
||||||
|
$gradient-text: $gradient-text-light;
|
||||||
|
|
||||||
// Services Section Variables
|
// Services Section Variables
|
||||||
$services-card-padding: 1.5rem;
|
$services-card-padding: 1.5rem;
|
||||||
$services-card-border-radius: 0.75rem;
|
$services-card-border-radius: 0.75rem;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue