sonarqube fixes

This commit is contained in:
Sascha 2025-10-30 17:20:24 +01:00
parent ce79a800f2
commit acbdb1ea3c
11 changed files with 205 additions and 341 deletions

View File

@ -14,7 +14,7 @@ type Props = {
export default function Navbar({ export default function Navbar({
theme, theme,
setTheme setTheme
}: Props) { }: Readonly<Props>) {
const [menuOpen, setMenuOpen] = useState(false); const [menuOpen, setMenuOpen] = useState(false);
const location = useLocation(); const location = useLocation();
const navigate = useNavigate(); const navigate = useNavigate();

View File

@ -71,7 +71,7 @@ export default function AboutSection(props: AboutSectionProps = {}) {
</div> </div>
{/* Personal Photo and Bio */} {/* Personal Photo and Bio */}
<div className="about-section__content" role="main"> <main className="about-section__content" role="main">
<div className="about-section__image-wrapper"> <div className="about-section__image-wrapper">
<img <img
src={profileImage} src={profileImage}
@ -88,19 +88,18 @@ export default function AboutSection(props: AboutSectionProps = {}) {
Hi, I'm {name}! Hi, I'm {name}!
</h3> </h3>
<div className="about-section__bio-text"> <div className="about-section__bio-text">
{bio.map((paragraph, index) => ( {bio.map((paragraph) => (
<p key={index}>{paragraph}</p> <p key={paragraph.substring(0, 50)}>{paragraph}</p>
))} ))}
</div> </div>
</div> </div>
</div> </div>
</div> </main>
{/* Feature Cards Grid */} {/* Feature Cards Grid */}
<div <div
id="features" id="features"
className="about-section__features" className="about-section__features"
role="region"
aria-labelledby="features-heading" aria-labelledby="features-heading"
> >
<h3 id="features-heading" className="sr-only">My Areas of Expertise</h3> <h3 id="features-heading" className="sr-only">My Areas of Expertise</h3>
@ -108,23 +107,21 @@ export default function AboutSection(props: AboutSectionProps = {}) {
<div className="about-section__features-grid"> <div className="about-section__features-grid">
{featureCards.map((card, index) => { {featureCards.map((card, index) => {
const IconComponent = card.icon; const IconComponent = card.icon;
const cardId = card.title.toLowerCase().replaceAll(/\s+/g, '-');
return ( return (
<article <article
key={index} key={cardId}
className={`about-section__feature-card about-section__feature-card--${card.colorClass}`} className={`about-section__feature-card about-section__feature-card--${card.colorClass}`}
role="group" aria-labelledby={`feature-${cardId}-title`}
aria-labelledby={`feature-${index}-title`} aria-describedby={`feature-${cardId}-description`}
aria-describedby={`feature-${index}-description`}
> >
<header className="about-section__feature-header"> <header className="about-section__feature-header">
<IconComponent <IconComponent
className={`about-section__feature-icon about-section__feature-icon--${card.colorClass}`} className={`about-section__feature-icon about-section__feature-icon--${card.colorClass}`}
aria-hidden="true" aria-hidden="true"
role="img"
aria-label={`${card.title} icon`}
/> />
<h4 <h4
id={`feature-${index}-title`} id={`feature-${cardId}-title`}
className={`about-section__feature-title about-section__feature-title--${card.colorClass}`} className={`about-section__feature-title about-section__feature-title--${card.colorClass}`}
> >
{card.title} {card.title}
@ -132,7 +129,7 @@ export default function AboutSection(props: AboutSectionProps = {}) {
</header> </header>
<div className="about-section__feature-content"> <div className="about-section__feature-content">
<p <p
id={`feature-${index}-description`} id={`feature-${cardId}-description`}
className={`about-section__feature-description about-section__feature-description--${card.colorClass}`} className={`about-section__feature-description about-section__feature-description--${card.colorClass}`}
> >
{card.description} {card.description}
@ -149,6 +146,6 @@ export default function AboutSection(props: AboutSectionProps = {}) {
</div> </div>
</div> </div>
</div> </div>
</section> </section >
); );
} }

View File

@ -83,16 +83,14 @@ export default function CertificationsSection(props: CertificationsSectionProps
</div> </div>
{/* Certifications Grid */} {/* Certifications Grid */}
<div <section
className="certifications-section__grid" className="certifications-section__grid"
role="region"
aria-label="Professional certifications and qualifications" aria-label="Professional certifications and qualifications"
> >
{sortedCertifications.map((cert, index) => ( {sortedCertifications.map((cert, index) => (
<article <div
key={index} key={`${cert.name}-${cert.year}-${cert.issuer}`}
className="certifications-section__card" className="certifications-section__card"
role="group"
aria-labelledby={`cert-${index}-title`} aria-labelledby={`cert-${index}-title`}
aria-describedby={`cert-${index}-details`} aria-describedby={`cert-${index}-details`}
> >
@ -108,7 +106,6 @@ export default function CertificationsSection(props: CertificationsSectionProps
<Award <Award
className="certifications-section__card-icon" className="certifications-section__card-icon"
aria-hidden="true" aria-hidden="true"
role="img"
aria-label="Certification award icon" aria-label="Certification award icon"
/> />
)} )}
@ -140,14 +137,13 @@ export default function CertificationsSection(props: CertificationsSectionProps
Certification {index + 1} of {sortedCertifications.length}. Certification {index + 1} of {sortedCertifications.length}.
{cert.name} from {cert.issuer}, completed in {cert.year}. {cert.name} from {cert.issuer}, completed in {cert.year}.
</div> </div>
</article>
))}
</div> </div>
))}
</section>
{/* Certifications summary for screen readers */} {/* Certifications summary for screen readers */}
<div className="certifications-section__summary sr-only" aria-live="polite"> <div className="certifications-section__summary sr-only" aria-live="polite">
<p> <p>
Certifications timeline: Spanning from {sortedCertifications[sortedCertifications.length - 1]?.year} to {sortedCertifications[0]?.year}, Certifications timeline: Spanning from {sortedCertifications.at(-1)?.year} to {sortedCertifications.at(0)?.year},
including {sortedCertifications.filter(cert => cert.issuer.includes('Udemy')).length} online courses and including {sortedCertifications.filter(cert => cert.issuer.includes('Udemy')).length} online courses and
{sortedCertifications.filter(cert => cert.issuer.includes('TU Darmstadt')).length} academic degree. {sortedCertifications.filter(cert => cert.issuer.includes('TU Darmstadt')).length} academic degree.
</p> </p>

View File

@ -1,4 +1,4 @@
import { Mail, Github, Linkedin, Send } from 'lucide-react'; import { Mail, ExternalLink, Send } from 'lucide-react';
import { personalConfig, getObfuscatedEmail } from '../../config/personal'; import { personalConfig, getObfuscatedEmail } from '../../config/personal';
import { getTexts } from '../../config/texts'; import { getTexts } from '../../config/texts';
@ -50,10 +50,10 @@ export default function ContactSection(props: ContactSectionProps = {}) {
announcement.textContent = 'Opening email client with pre-filled message'; announcement.textContent = 'Opening email client with pre-filled message';
document.body.appendChild(announcement); document.body.appendChild(announcement);
window.location.href = `mailto:${email}?subject=${subject}&body=${body}`; globalThis.location.href = `mailto:${email}?subject=${subject}&body=${body}`;
setTimeout(() => { setTimeout(() => {
document.body.removeChild(announcement); announcement.remove();
}, 1000); }, 1000);
}; };
@ -70,10 +70,10 @@ export default function ContactSection(props: ContactSectionProps = {}) {
announcement.textContent = `Opening ${platform} profile in new tab`; announcement.textContent = `Opening ${platform} profile in new tab`;
document.body.appendChild(announcement); document.body.appendChild(announcement);
window.open(url, '_blank', 'noopener,noreferrer'); globalThis.open(url, '_blank', 'noopener,noreferrer');
setTimeout(() => { setTimeout(() => {
document.body.removeChild(announcement); announcement.remove();
}, 1000); }, 1000);
}; };
@ -98,15 +98,13 @@ export default function ContactSection(props: ContactSectionProps = {}) {
</div> </div>
{/* Contact Content Grid */} {/* Contact Content Grid */}
<div className="contact-section__grid" role="main"> <main className="contact-section__grid">
{/* Let's Connect Card */} {/* Let's Connect Card */}
<div className="contact-section__connect-card" role="region" aria-labelledby="connect-heading"> <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 <Mail
className="contact-section__email-icon" className="contact-section__email-icon"
aria-hidden="true" aria-hidden="true"
role="img"
aria-label="Email icon"
/> />
<div className="contact-section__connect-text"> <div className="contact-section__connect-text">
<h3 id="connect-heading" className="contact-section__email-title"> <h3 id="connect-heading" className="contact-section__email-title">
@ -141,15 +139,13 @@ export default function ContactSection(props: ContactSectionProps = {}) {
<div className="contact-section__info-card"> <div className="contact-section__info-card">
<h3 id="info-heading" className="sr-only">Contact Information</h3> <h3 id="info-heading" className="sr-only">Contact Information</h3>
<div className="contact-section__availability" role="status" aria-live="polite"> <output className="contact-section__availability" aria-live="polite">
<div <div
className="contact-section__status-indicator" className="contact-section__status-indicator"
aria-hidden="true" aria-hidden="true"
role="img"
aria-label="Available status indicator"
></div> ></div>
<span className="contact-section__status-text">{availabilityText}</span> <span className="contact-section__status-text">{availabilityText}</span>
</div> </output>
<div className="contact-section__quick-facts"> <div className="contact-section__quick-facts">
<dl className="contact-section__fact-list"> <dl className="contact-section__fact-list">
@ -171,11 +167,11 @@ export default function ContactSection(props: ContactSectionProps = {}) {
</aside> </aside>
{/* Social Contact Methods */} {/* Social Contact Methods */}
<div className="contact-section__social-card" role="region" aria-labelledby="social-heading"> <div className="contact-section__social-card" aria-labelledby="social-heading">
<h4 id="social-heading" className="contact-section__social-title">{socialTitle}</h4> <h4 id="social-heading" className="contact-section__social-title">{socialTitle}</h4>
<div className="contact-section__social-list" role="list"> <ul className="contact-section__social-list">
<div className="contact-section__detail-item" role="listitem"> <li className="contact-section__detail-item">
<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}
@ -187,38 +183,38 @@ export default function ContactSection(props: ContactSectionProps = {}) {
<span className="contact-section__detail-text" aria-label="Email address"> <span className="contact-section__detail-text" aria-label="Email address">
{getObfuscatedEmail()} {getObfuscatedEmail()}
</span> </span>
</div> </li>
<div className="contact-section__detail-item" role="listitem"> <li className="contact-section__detail-item">
<button <button
className="contact-section__social-button contact-section__social-button--github" className="contact-section__social-button contact-section__social-button--github"
onClick={() => handleSocialClick('GitHub', personalConfig.social.github.url)} onClick={() => handleSocialClick('GitHub', personalConfig.social.github.url)}
aria-label={`Visit GitHub profile: ${personalConfig.social.github.username} (opens in new tab)`} aria-label={`Visit GitHub profile: ${personalConfig.social.github.username} (opens in new tab)`}
type="button" type="button"
> >
<Github className="contact-section__social-icon" aria-hidden="true" /> <ExternalLink className="contact-section__social-icon" aria-hidden="true" />
</button> </button>
<span className="contact-section__detail-text" aria-label="GitHub username"> <span className="contact-section__detail-text" aria-label="GitHub username">
{personalConfig.social.github.username} {personalConfig.social.github.username}
</span> </span>
</div> </li>
<div className="contact-section__detail-item" role="listitem"> <li className="contact-section__detail-item">
<button <button
className="contact-section__social-button contact-section__social-button--linkedin" className="contact-section__social-button contact-section__social-button--linkedin"
onClick={() => handleSocialClick('LinkedIn', personalConfig.social.linkedin.url)} onClick={() => handleSocialClick('LinkedIn', personalConfig.social.linkedin.url)}
aria-label={`Visit LinkedIn profile: ${personalConfig.social.linkedin.username} (opens in new tab)`} aria-label={`Visit LinkedIn profile: ${personalConfig.social.linkedin.username} (opens in new tab)`}
type="button" type="button"
> >
<Linkedin className="contact-section__social-icon" aria-hidden="true" /> <ExternalLink className="contact-section__social-icon" aria-hidden="true" />
</button> </button>
<span className="contact-section__detail-text" aria-label="LinkedIn username"> <span className="contact-section__detail-text" aria-label="LinkedIn username">
{personalConfig.social.linkedin.username} {personalConfig.social.linkedin.username}
</span> </span>
</li>
</ul>
</div> </div>
</div> </main>
</div>
</div>
</div> </div>
</section> </section>
); );

View File

@ -39,7 +39,7 @@ export default function HeroSection(props: HeroSectionProps = {}) {
document.body.appendChild(announcement); document.body.appendChild(announcement);
setTimeout(() => { setTimeout(() => {
document.body.removeChild(announcement); announcement.remove();
}, 1000); }, 1000);
}; };
@ -63,7 +63,7 @@ export default function HeroSection(props: HeroSectionProps = {}) {
link.click(); link.click();
// Clean up // Clean up
document.body.removeChild(link); link.remove();
console.log('✅ Resume download initiated'); console.log('✅ Resume download initiated');
announceToScreenReader('Resume download started successfully'); announceToScreenReader('Resume download started successfully');
@ -93,7 +93,7 @@ export default function HeroSection(props: HeroSectionProps = {}) {
}; };
return ( return (
<section className="hero-section" role="main" aria-labelledby="hero-title"> <main className="hero-section" aria-labelledby="hero-title">
<div className="hero-section__container"> <div className="hero-section__container">
{/* Skip link for keyboard navigation */} {/* Skip link for keyboard navigation */}
<div className="hero-section__skip"> <div className="hero-section__skip">
@ -167,6 +167,6 @@ export default function HeroSection(props: HeroSectionProps = {}) {
))} ))}
</aside> </aside>
</div> </div>
</section> </main>
); );
} }

View File

@ -1,4 +1,4 @@
import { Github, ExternalLink } from 'lucide-react'; import { GitBranch, ExternalLink } from 'lucide-react';
import { useState, useRef, useEffect, type KeyboardEvent } from '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';
@ -167,7 +167,7 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) {
document.body.appendChild(announcement); document.body.appendChild(announcement);
setTimeout(() => { setTimeout(() => {
document.body.removeChild(announcement); announcement.remove();
}, 1000); }, 1000);
}; };
@ -248,9 +248,8 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) {
<div className="projects-section__overlay" aria-hidden="true"></div> <div className="projects-section__overlay" aria-hidden="true"></div>
{/* Conditionally visible action buttons */} {/* Conditionally visible action buttons */}
<div <fieldset
className={`projects-section__actions ${isActive ? 'projects-section__actions--visible' : ''}`} className={`projects-section__actions ${isActive ? 'projects-section__actions--visible' : ''}`}
role="group"
aria-label={`Actions for ${project.title}`} aria-label={`Actions for ${project.title}`}
aria-hidden={!isActive} aria-hidden={!isActive}
> >
@ -265,7 +264,7 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) {
tabIndex={isActive ? 0 : -1} tabIndex={isActive ? 0 : -1}
onKeyDown={(e) => handleActionKeyDown(e, cardIndex, 0)} onKeyDown={(e) => handleActionKeyDown(e, cardIndex, 0)}
> >
<Github className="projects-section__action-icon" aria-hidden="true" /> <GitBranch className="projects-section__action-icon" aria-hidden="true" />
<span className="projects-section__action-text">{texts.codeButtonText}</span> <span className="projects-section__action-text">{texts.codeButtonText}</span>
</a> </a>
)} )}
@ -283,8 +282,9 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) {
<ExternalLink className="projects-section__action-icon" aria-hidden="true" /> <ExternalLink className="projects-section__action-icon" aria-hidden="true" />
<span className="projects-section__action-text">{texts.liveButtonText}</span> <span className="projects-section__action-text">{texts.liveButtonText}</span>
</a> </a>
)} )}
</div> </fieldset>
</div> </div>
<div className="projects-section__content"> <div className="projects-section__content">
@ -305,18 +305,17 @@ export default function ProjectsSection(props: ProjectsSectionProps = {}) {
</div> </div>
</div> </div>
<div className="projects-section__technologies" role="list" aria-label="Technologies used"> <ul className="projects-section__technologies" aria-label="Technologies used">
{project.technologies.map((tech, index) => ( {project.technologies.map((tech, index) => (
<span <li
key={tech} key={tech}
className="projects-section__tech-badge" className="projects-section__tech-badge"
role="listitem"
aria-label={`Technology ${index + 1}: ${tech}`} aria-label={`Technology ${index + 1}: ${tech}`}
> >
{tech} {tech}
</span> </li>
))} ))}
</div> </ul>
{/* Enhanced status indicator for active card */} {/* Enhanced status indicator for active card */}
{isActive && ( {isActive && (

View File

@ -85,10 +85,8 @@ export default function ServicesSection(props: ServicesSectionProps = {}) {
const IconComponent = service.icon; const IconComponent = service.icon;
return ( return (
<article <article
key={index} key={`${service.title}-${service.colorClass}`}
className={`services-section__card services-section__card--${service.colorClass}`} className={`services-section__card services-section__card--${service.colorClass}`}
tabIndex={0}
role="group"
aria-labelledby={`service-${index}-title`} aria-labelledby={`service-${index}-title`}
aria-describedby={`service-${index}-description`} aria-describedby={`service-${index}-description`}
> >
@ -96,8 +94,6 @@ export default function ServicesSection(props: ServicesSectionProps = {}) {
<IconComponent <IconComponent
className={`services-section__icon services-section__icon--${service.colorClass}`} className={`services-section__icon services-section__icon--${service.colorClass}`}
aria-hidden="true" aria-hidden="true"
role="img"
aria-label={`${service.title} service icon`}
/> />
<h3 <h3
id={`service-${index}-title`} id={`service-${index}-title`}

View File

@ -120,10 +120,9 @@ export default function SkillsSection(props: SkillsSectionProps = {}) {
{skillCategories.map((category, categoryIndex) => { {skillCategories.map((category, categoryIndex) => {
const colorClass = getColorByPosition(categoryIndex); const colorClass = getColorByPosition(categoryIndex);
return ( return (
<article <fieldset
key={category.title} key={category.title}
className={`skills-section__category skills-section__category--${colorClass}`} className={`skills-section__category skills-section__category--${colorClass}`}
role="group"
aria-labelledby={`category-${categoryIndex}-title`} aria-labelledby={`category-${categoryIndex}-title`}
> >
<header className="skills-section__category-header"> <header className="skills-section__category-header">
@ -139,18 +138,16 @@ export default function SkillsSection(props: SkillsSectionProps = {}) {
</div> </div>
</header> </header>
<div <ul
className="skills-section__skills-list" className="skills-section__skills-list"
role="list"
aria-label={`${category.title} skills`} aria-label={`${category.title} skills`}
> >
{category.skills.map((skillItem, skillIndex) => { {category.skills.map((skillItem, skillIndex) => {
const displayLevel = calculateSkillLevel(skillItem.value); const displayLevel = calculateSkillLevel(skillItem.value);
return ( return (
<div <li
key={skillItem.skill} key={skillItem.skill}
className="skills-section__skill-item" className="skills-section__skill-item"
role="listitem"
aria-label={`${skillItem.skill}: ${displayLevel} level`} aria-label={`${skillItem.skill}: ${displayLevel} level`}
> >
<div className="skills-section__skill-header"> <div className="skills-section__skill-header">
@ -195,11 +192,12 @@ export default function SkillsSection(props: SkillsSectionProps = {}) {
Proficiency level: {displayLevel} at {skillItem.value} percent. Proficiency level: {displayLevel} at {skillItem.value} percent.
</div> </div>
</div> </div>
</div> </li>
); )
})} })}
</div> </ul>
</article>
</fieldset>
); );
})} })}
</div> </div>

View File

@ -278,17 +278,23 @@
transform: translateY(-1px); transform: translateY(-1px);
} }
// Enhanced focus states for AAA compliance
&:focus {
outline: 3px solid var(--color-focus-ring, #2563eb);
outline-offset: 2px;
}
// Brand-specific icon colors with better contrast // Brand-specific icon colors with better contrast
&--email .contact-section__social-icon { &--email .contact-section__social-icon {
color: #2563eb; // Stronger blue for email color: var(--social-icon-email, #2563eb); // Use theme variable
} }
&--github .contact-section__social-icon { &--github .contact-section__social-icon {
color: #1f2937; // Darker for GitHub color: var(--social-icon-github, #1f2937); // Use theme variable
} }
&--linkedin .contact-section__social-icon { &--linkedin .contact-section__social-icon {
color: #0d67b5; // Stronger LinkedIn blue color: var(--social-icon-linkedin, #0d67b5); // Use theme variable
} }
} }
@ -500,7 +506,7 @@
padding: 0; padding: 0;
} }
&__email-icon { &__email-card-icon {
width: 3rem; width: 3rem;
height: 3rem; height: 3rem;
color: var(--primary-color); color: var(--primary-color);
@ -508,19 +514,6 @@
display: block; display: block;
} }
&__email-title {
font-size: 1.5rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
&__email-description {
color: var(--text-secondary);
font-size: 1rem;
line-height: 1.6;
}
&__email-button { &__email-button {
background: var(--contact-button-bg); background: var(--contact-button-bg);
border: none; border: none;
@ -669,26 +662,6 @@
white-space: normal !important; white-space: normal !important;
background-color: var(--contact-button-bg) !important; background-color: var(--contact-button-bg) !important;
color: var(--contact-button-text) !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 // Enhanced focus states for AAA compliance
&:focus { &:focus {
outline: 3px solid var(--color-focus-ring, #2563eb); outline: 3px solid var(--color-focus-ring, #2563eb);

View File

@ -56,8 +56,9 @@
border-radius: $card-border-radius-md; border-radius: $card-border-radius-md;
box-shadow: var(--projects-card-shadow); box-shadow: var(--projects-card-shadow);
overflow: hidden; overflow: hidden;
transition: $transition-base; transition: all 0.3s ease;
border: 1px solid var(--projects-card-border); border: 1px solid var(--projects-card-border);
cursor: pointer;
&:hover { &:hover {
transform: translateY(-0.25rem); transform: translateY(-0.25rem);
@ -73,7 +74,7 @@
.projects-section__actions { .projects-section__actions {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translate(-50%, -50%) translateY(0);
pointer-events: all; pointer-events: all;
} }
} }
@ -89,7 +90,7 @@
.projects-section__actions { .projects-section__actions {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translate(-50%, -50%) translateY(0);
pointer-events: all; pointer-events: all;
} }
} }
@ -106,7 +107,7 @@
.projects-section__actions { .projects-section__actions {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translate(-50%, -50%) translateY(0);
pointer-events: all; pointer-events: all;
} }
} }
@ -118,7 +119,7 @@
.projects-section__actions { .projects-section__actions {
opacity: 1; opacity: 1;
transform: translateY(0); transform: translate(-50%, -50%) translateY(0);
pointer-events: all; pointer-events: all;
} }
} }
@ -404,65 +405,6 @@
border: 0 !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 // High contrast mode
@media (prefers-contrast: high) { @media (prefers-contrast: high) {
.projects-section__card { .projects-section__card {

View File

@ -106,6 +106,7 @@
min-height: 250px; // Ensures consistent card heights min-height: 250px; // Ensures consistent card heights
position: relative; position: relative;
border: 1px solid rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.2);
animation: cardFadeIn 0.6s ease-out;
// Add subtle backdrop blur for depth // Add subtle backdrop blur for depth
&::before { &::before {
@ -132,6 +133,52 @@
outline-offset: 2px; outline-offset: 2px;
} }
// 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
}
}
// Animation delays for cards
&: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;
}
// Responsive card heights - more flexible approach // Responsive card heights - more flexible approach
@media (min-width: 768px) { @media (min-width: 768px) {
min-height: 280px; // Use min-height instead of fixed height min-height: 280px; // Use min-height instead of fixed height
@ -150,6 +197,54 @@
max-height: 400px; max-height: 400px;
} }
// High contrast mode support
@media (prefers-contrast: high) {
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;
}
}
// Reduced motion support
@media (prefers-reduced-motion: reduce) {
transition: none !important;
&:hover,
&:focus {
transform: none !important;
}
}
// Touch device optimizations
@media (hover: none) and (pointer: coarse) {
min-height: 320px;
&:hover {
transform: none;
box-shadow: $shadow-card;
}
&:focus {
transform: translateY(-2px);
box-shadow: $shadow-card-hover;
outline: 3px solid var(--color-focus-ring, #2563eb);
outline-offset: 2px;
}
}
// Dark theme enhancements
@media (prefers-color-scheme: dark) {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3),
0 2px 4px -1px rgba(0, 0, 0, 0.2);
}
&--primary { &--primary {
background: var(--service-card-primary-bg); background: var(--service-card-primary-bg);
} }
@ -277,131 +372,25 @@
flex: 1; flex: 1;
display: flex; display: flex;
align-items: center; align-items: center;
// ================================
// WCAG Level AAA Accessibility Enhancements
// ================================
@media (max-width: 767px) { // High contrast mode support for better accessibility
padding: 0 1.25rem 1.25rem 1.25rem; @media (prefers-contrast: high) {
// Ensure text remains readable in high contrast mode
.services-section__card-title,
.services-section__card-description {
text-shadow: none !important;
} }
} }
animation-delay: 0.4s;
&__card-description {
margin: 0;
line-height: 1.5;
font-size: 0.875rem;
text-align: center;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
font-weight: 400;
@media (max-width: 767px) {
font-size: 0.8rem;
line-height: 1.4;
} }
&:nth-child(5) {
@media (min-width: 1280px) { animation-delay: 0.5s;
font-size: 0.9rem;
line-height: 1.6;
}
&--primary {
color: var(--service-description-primary);
}
&--secondary {
color: var(--service-description-secondary);
}
&--tertiary {
color: var(--service-description-tertiary);
}
&--quaternary {
color: var(--service-description-quaternary);
}
&--quinary {
color: var(--service-description-quinary);
}
&--senary {
color: var(--service-description-senary);
}
}
}
// Handle 6 cards specifically for better distribution
.services-section__grid:has(.services-section__card:nth-child(6):last-child) {
// For exactly 6 cards
@media (min-width: 768px) and (max-width: 1023px) {
grid-template-columns: repeat(2, 1fr);
// Make last 2 cards center themselves if needed
.services-section__card:nth-child(5),
.services-section__card:nth-child(6) {
justify-self: center;
}
}
@media (min-width: 1024px) {
grid-template-columns: repeat(3, 1fr);
}
}
// Ultra-compact mobile layout for very small screens
@media (max-height: 700px) and (max-width: 767px) {
.services-section {
min-height: auto;
padding: 1rem 0;
&__header {
margin-bottom: 1rem;
}
&__card {
min-height: 200px;
height: auto;
}
&__card-header {
padding: 1rem;
}
&__icon {
width: 1.75rem;
height: 1.75rem;
margin-bottom: 0.5rem;
}
}
}
// ================================
// 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
} }
&:nth-child(6) {
animation-delay: 0.6s;
} }
} }
@ -468,29 +457,6 @@
// Enhance card visibility with better shadows in dark mode // Enhance card visibility with better shadows in dark mode
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3),
0 2px 4px -1px rgba(0, 0, 0, 0.2); 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; animation: cardFadeIn 0.6s ease-out;
&:nth-child(1) { &:nth-child(1) {
@ -511,4 +477,5 @@
&:nth-child(6) { &:nth-child(6) {
animation-delay: 0.6s; animation-delay: 0.6s;
} }
}
} }