fix(a11y): reconcile responsive layouts and multilingual UI
This commit is contained in:
59 files changed
+27178
-187
No files matched your search
@@ -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; }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user