fix(a11y): reconcile responsive layouts and multilingual UI

This commit is contained in:
DeCentN2Madness committed 2026-10-15 12:18:44 -07:00
1 parent 8e64d32209
commit 59911eb929
59 files changed
+27178 -187

No files matched your search

+2 -2
View File
@@ -21,7 +21,7 @@ const readMore = { en: 'Read More', es: 'Leer Más', ar: 'اقرأ المزيد'
<p>{post.data.description}</p>
<div class="blog-card-footer" style="align-items:center;display:flex;justify-content:space-between;gap:var(--space-lg);margin-top:auto;">
<a class="text-link" href={href}>{readMore}</a>
<LikeViewCounter slug={post.data.slug} />
<LikeViewCounter slug={post.data.slug} lang={post.data.lang} />
</div>
</div>
</article>
@@ -29,7 +29,7 @@ const readMore = { en: 'Read More', es: 'Leer Más', ar: 'اقرأ المزيد'
<style>
.author-byline { color: inherit; text-decoration: none; }
.author-byline:hover { text-decoration: underline; }
.author-byline:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 3px; }
.author-byline:focus-visible { outline: 3px solid var(--color-primary); outline-offset: 3px; }
.blog-card-meta { align-items: center; display: flex; font-size: .78rem; gap: .75rem; }
.blog-card-meta img { aspect-ratio: 1; border-radius: 50%; height: 42px; width: 42px; }
.blog-card { background: white; border: 0; border-radius: 4px; box-shadow: rgba(54, 48, 48, 0.28) 0 0 12px 0; display: flex; margin-bottom: 40px; overflow: hidden; }
+12 -10
View File
@@ -1,5 +1,6 @@
---
import footerData from '../data/blog-footers.json';
import { articleMessages } from '../utils/article-messages';
interface FooterLink {
title: string;
@@ -17,27 +18,28 @@ interface FooterData {
related: RelatedPost[];
}
const { entryId } = Astro.props as { entryId: string };
const { entryId, lang } = Astro.props as { entryId: string; lang: string };
const text = articleMessages(lang);
const footer = (footerData as Record<string, FooterData>)[entryId];
---
{footer && (
<footer class="blog-post-footer">
{(footer.previous || footer.next) && (
<nav class="post-pagination" aria-label="Article navigation">
<nav class="post-pagination" aria-label={text.navigation}>
<div class="post-pagination__item post-pagination__item--previous">
{footer.previous && (
<>
<span>Previous Post</span>
<h2><a href={footer.previous.href}>{footer.previous.title}</a></h2>
<span>{text.previous}</span>
<h2><a href={footer.previous.href}><bdi>{footer.previous.title}</bdi></a></h2>
</>
)}
</div>
<div class="post-pagination__item post-pagination__item--next">
{footer.next && (
<>
<span>Next Post</span>
<h2><a href={footer.next.href}>{footer.next.title}</a></h2>
<span>{text.next}</span>
<h2><a href={footer.next.href}><bdi>{footer.next.title}</bdi></a></h2>
</>
)}
</div>
@@ -47,14 +49,14 @@ const footer = (footerData as Record<string, FooterData>)[entryId];
{footer.related.length > 0 && (
<section class="related-posts" aria-labelledby={`related-posts-${entryId.replace('/', '-')}`}>
<div class="container">
<h2 id={`related-posts-${entryId.replace('/', '-')}`}>Similar Blog Posts</h2>
<h2 id={`related-posts-${entryId.replace('/', '-')}`}>{text.related}</h2>
<div class="related-posts__list">
{footer.related.map((post) => (
<article class="related-post">
<a class="related-post__image-link" href={post.href} aria-label={`Read full post: ${post.title}`}>
<a class="related-post__image-link" href={post.href} aria-label={`${text.readFull}: ${post.title}`}>
<img src={post.image} alt={post.alt} loading="lazy" />
</a>
<h3><a href={post.href}>{post.title}</a></h3>
<h3><a href={post.href}><bdi>{post.title}</bdi></a></h3>
</article>
))}
</div>
@@ -104,7 +106,7 @@ const footer = (footerData as Record<string, FooterData>)[entryId];
.post-pagination a:focus-visible,
.related-post a:hover,
.related-post a:focus-visible {
color: var(--color-accent);
color: var(--color-primary-dark);
}
.related-posts {
+6
View File
@@ -30,6 +30,12 @@ const title = Astro.props.title ?? text.contactHeading;
</form>
<style>
.contact-form-card__note :global(a) {
color: var(--color-primary);
text-decoration: underline;
text-underline-offset: .15em;
}
.contact-form-card {
background: white;
border-radius: 4px;
+4 -2
View File
@@ -561,12 +561,14 @@ if (schema) {
}
.faq-accordion--source .faq-answer__inner :global(a) {
color: var(--color-accent-strong);
color: var(--color-primary);
text-decoration: underline;
text-underline-offset: .15em;
}
.faq-accordion--source .faq-answer__inner :global(a:hover),
.faq-accordion--source .faq-answer__inner :global(a:focus-visible) {
color: var(--color-accent);
color: var(--color-primary-dark);
}
.faq-accordion--page {
+93
View File
@@ -0,0 +1,93 @@
---
interface Slide { src: string; alt: string; caption: string }
interface Props { id: string; label: string; slides: Slide[] }
const { id, label, slides } = Astro.props;
---
<section class="image-carousel" id={id} aria-label={label} data-image-carousel>
<div class="image-carousel__stage">
{slides.map((slide, index) => (
<figure id={`${id}-slide-${index + 1}`} data-slide>
<img src={slide.src} alt={slide.alt} width="948" height="534" loading={index === 0 ? 'eager' : 'lazy'} />
<figcaption>{slide.caption}</figcaption>
</figure>
))}
<div data-carousel-controls hidden>
<button class="image-carousel__previous" type="button" aria-label="Previous slide" data-previous><span aria-hidden="true">❮</span></button>
<button class="image-carousel__next" type="button" aria-label="Next slide" data-next><span aria-hidden="true">❯</span></button>
</div>
</div>
<div data-carousel-controls hidden>
<div class="image-carousel__thumbnails" role="group" aria-label="Choose a tour photo">
{slides.map((slide, index) => (
<button type="button" data-thumbnail aria-label={`Show photo ${index + 1}: ${slide.caption}`} aria-controls={`${id}-slide-${index + 1}`}>
<img src={slide.src} alt="" width="80" height="80" loading="lazy" />
</button>
))}
</div>
<p class="sr-only" data-carousel-status role="status" aria-atomic="true"></p>
</div>
</section>
<script>
document.querySelectorAll<HTMLElement>('[data-image-carousel]').forEach(carousel => {
const slides = [...carousel.querySelectorAll<HTMLElement>('[data-slide]')];
const thumbnails = [...carousel.querySelectorAll<HTMLButtonElement>('[data-thumbnail]')];
const status = carousel.querySelector<HTMLElement>('[data-carousel-status]')!;
let current = 0;
const show = (index: number, announce = true) => {
current = (index + slides.length) % slides.length;
slides.forEach((slide, i) => {
slide.hidden = i !== current;
thumbnails[i].setAttribute('aria-pressed', String(i === current));
});
if (announce) status.textContent = `Photo ${current + 1} of ${slides.length}: ${slides[current].querySelector('figcaption')?.textContent}`;
};
slides.forEach((slide, index) => {
slide.setAttribute('role', 'group');
slide.setAttribute('aria-roledescription', 'slide');
slide.setAttribute('aria-label', `${index + 1} of ${slides.length}`);
});
carousel.setAttribute('aria-roledescription', 'carousel');
carousel.querySelector('[data-previous]')!.addEventListener('click', () => show(current - 1));
carousel.querySelector('[data-next]')!.addEventListener('click', () => show(current + 1));
thumbnails.forEach((button, index) => button.addEventListener('click', () => show(index)));
carousel.addEventListener('keydown', event => {
if (!(event.target instanceof HTMLButtonElement)) return;
if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
event.preventDefault(); show(current + (event.key === 'ArrowRight' ? 1 : -1));
}
});
show(0, false);
carousel.dataset.enhanced = 'true';
carousel.querySelectorAll<HTMLElement>('[data-carousel-controls]').forEach(control => control.hidden = false);
});
</script>
<style>
.image-carousel { max-width: 1000px; margin: 2rem auto; }
.image-carousel__stage { position: relative; }
.image-carousel [data-carousel-status] { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
.image-carousel figure { position: relative; margin: 0 0 2rem; }
.image-carousel [hidden] { display: none !important; }
.image-carousel figure img { display: block; width: 100%; height: auto; margin: 0; }
.image-carousel figcaption { margin: 0; padding: .5rem .75rem; background: var(--color-primary); color: white; text-align: center; font-size: 1rem; line-height: 1.4; }
.image-carousel[data-enhanced] figure { margin-bottom: 1rem; }
.image-carousel[data-enhanced] figcaption { position: absolute; bottom: 0; width: 100%; background: rgb(37 64 128 / 90%); }
.image-carousel button { cursor: pointer; }
.image-carousel__previous, .image-carousel__next { position: absolute; top: 50%; transform: translateY(-50%); min-width: 44px; min-height: 44px; padding: 1rem; border: 0; background: rgb(255 255 255 / 95%); color: var(--color-primary); font-size: 2rem; line-height: 1; border-radius: 3px; }
.image-carousel__previous { left: 0; }
.image-carousel__next { right: 0; }
.image-carousel__previous:hover, .image-carousel__next:hover { background: var(--color-primary); color: white; }
.image-carousel button:focus-visible { outline: 3px solid var(--color-primary); outline-offset: 3px; box-shadow: 0 0 0 6px white; }
.image-carousel__thumbnails { display: flex; flex-wrap: wrap; justify-content: center; gap: 6px; padding: 6px; }
.image-carousel__thumbnails button { padding: 2px; border: 3px solid transparent; background: white; border-radius: 0; }
.image-carousel__thumbnails img { display: block; width: 80px; height: 80px; object-fit: cover; margin: 0; opacity: .6; }
.image-carousel__thumbnails button[aria-pressed="true"] { border-color: var(--color-primary); }
.image-carousel__thumbnails button:is(:hover, [aria-pressed="true"]) img { opacity: 1; }
@media (max-width: 479px) {
.image-carousel__previous, .image-carousel__next { padding: .75rem; font-size: 1rem; }
.image-carousel[data-enhanced] figcaption { position: static; font-size: .875rem; }
.image-carousel__previous, .image-carousel__next { top: 30%; }
}
/* No automatic rotation or animation: reduced-motion users get the same stable slides. */
</style>
+2 -2
View File
@@ -46,12 +46,12 @@ const hasAlternatives = choices.some(({ route }) => route !== pathname);
.lang-switcher summary, .lang-switcher__static { align-items: center; color: var(--color-primary); display: flex; font-size: .85rem; font-weight: 700; gap: .35rem; min-height: 44px; }
.lang-switcher summary { cursor: pointer; list-style: none; }
.lang-switcher summary::-webkit-details-marker { display: none; }
.lang-switcher summary:focus-visible, .lang-switcher__menu a:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 3px; }
.lang-switcher summary:focus-visible, .lang-switcher__menu a:focus-visible { outline: 3px solid var(--color-primary); outline-offset: 3px; }
.lang-switcher summary svg { flex-shrink: 0; }
.lang-switcher__menu { background: white; box-shadow: var(--shadow-md); inset-inline-start: 50%; list-style: none; margin: 0; min-width: 150px; padding: .5rem 0 .4rem; position: absolute; top: 100%; transform: translateX(-50%); z-index: 10; }
:global(html[dir="rtl"]) .lang-switcher__menu { transform: translateX(50%); }
.lang-switcher__menu a { color: rgb(83 83 91); display: flex; flex-direction: column; font-size: .85rem; padding: .45rem .9rem; text-decoration: none; white-space: nowrap; }
.lang-switcher__menu a:hover { background: var(--color-primary); color: white; }
.lang-switcher__menu a[aria-current="page"] { color: var(--color-accent-strong); font-weight: 700; }
.lang-switcher__menu a[aria-current="page"] { color: var(--color-primary); font-weight: 700; text-decoration: underline; }
.lang-switcher__menu small { font-size: .68rem; font-weight: 400; }
</style>
+8 -5
View File
@@ -1,11 +1,14 @@
---
const { slug } = Astro.props;
import { articleMessages } from '../utils/article-messages';
interface Props { slug: string; lang: string }
const { slug, lang } = Astro.props;
const text = articleMessages(lang);
---
<div class="like-view-counter" data-counter>
<span aria-label="Article views"><span class="view-count" data-views>0</span> views</span>
<span class="like-group">
<span class="like-count" data-likes>—</span>
<button class="like-button" type="button" data-like data-slug={slug} aria-label="Like this article" aria-pressed="false">
<span aria-label={text.articleViews}><span class="view-count" data-views dir={lang === 'ar' ? 'ltr' : undefined}>0</span> {text.views}</span>
<span class="like-group" role={lang === 'ar' ? 'group' : undefined} aria-label={lang === 'ar' ? text.likes : undefined}>
<span class="like-count" data-likes dir={lang === 'ar' ? 'ltr' : undefined}>—</span>
<button class="like-button" type="button" data-like data-slug={slug} aria-label={text.like} aria-pressed="false">
<svg aria-hidden="true" viewBox="0 0 12 10" width="20" height="20" focusable="false" class="like-icon">
<path d="m1 4.4c-1-4 2.5-5 4.5-2.5l.5 .6l.5-.6c2-2.5 6-1.5 4.5 2.5q-1 3-5 5q-4-2-5-5z"></path>
</svg>
+2 -2
View File
@@ -40,12 +40,12 @@ const navLabel = { en: 'Main navigation', es: 'Navegación principal', ar: 'ال
.main-nav ul { display: flex; gap: 1.4rem; list-style: none; margin: 0; padding: 0; }
.main-nav li { position: relative; }
.main-nav a { color: var(--color-primary); font-size: .9rem; text-decoration: none; }
.main-nav a:hover { color: var(--color-accent-strong); }
.main-nav a:hover { color: var(--color-primary-dark); }
.nav-fallback { border: 1px solid currentColor; border-radius: 2px; font-size: .58rem; line-height: 1; margin-inline-start: .35rem; padding: .1rem .2rem; text-transform: uppercase; }
.main-nav .submenu { background: white; box-shadow: var(--shadow-md); display: none; gap: 0; inset-inline-start: -1rem; min-width: 180px; padding: .75rem 0; position: absolute; top: 100%; }
.main-nav .submenu a { display: block; padding: .45rem 1rem; }
.main-nav li:hover > .submenu, .main-nav li:focus-within > .submenu { display: block; }
.main-nav a:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 3px; }
.main-nav a:focus-visible { outline: 3px solid var(--color-primary); outline-offset: 3px; }
@media (max-width: 950px) {
.main-nav { display: none; }
:global(.mobile-nav) .main-nav { display: block; }
+1 -1
View File
@@ -23,7 +23,7 @@ const menu = { en: 'Menu', es: 'Menú', ar: 'القائمة' }[lang];
<style>
.mobile-nav { display: none; }
.mobile-nav summary:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 3px; }
.mobile-nav summary:focus-visible { outline: 3px solid var(--color-primary); outline-offset: 3px; }
@media (max-width: 950px) {
.mobile-nav { display: block; }
}
@@ -9,6 +9,11 @@ const { section } = Astro.props;
.director-card img { border-radius: 3px; height: auto; margin: 0 auto; width: 100%; }
.director-card h3 { font-family: var(--font-body); font-size: .84rem; letter-spacing: .5px; margin-top: 1rem; }
.signature { max-width: 270px; }
/* Keep the shared mobile stack despite this component's scoped grid rule. */
@media (max-width: 760px) {
.director-layout { grid-template-columns: minmax(0, 1fr); }
.signature { width: 100%; }
}
</style>
<section class="section director-section">
@@ -81,6 +81,12 @@ const text = formMessages(lang);
margin: 0 0 25px;
}
.contact-page__copy :global(a:not(.content-button)) {
color: var(--color-primary);
text-decoration: underline;
text-underline-offset: .15em;
}
.contact-page__copy :global(.content-button) {
margin-bottom: 40px;
}