feat(i18n): reconcile multilingual navigation and RTL rendering
This commit is contained in:
15 files changed
+806
-232
No files matched your search
@@ -1,46 +1,62 @@
|
||||
---
|
||||
import ContactObfuscation from './ContactObfuscation.astro';
|
||||
import site from '../data/site.json';
|
||||
import { fallbackLabel, resolveNavigationDestination, type VisitorLocale, type VisitorPublication } from '../utils/visitor-navigation';
|
||||
const year = new Date().getFullYear();
|
||||
const { lang = 'en' } = Astro.props;
|
||||
interface Props { lang?: VisitorLocale; manifest: VisitorPublication[]; }
|
||||
const { lang = 'en', manifest } = Astro.props;
|
||||
const labels = lang === 'es'
|
||||
? { about: 'Información', overview: 'Acerca de AIA', team: 'Equipo', careers: 'Carreras', giveback: 'Devolver', resources: 'Recursos', forms: 'Formularios', insurance: 'Seguros', referrals: 'Referencias', employee: 'Portal para empleados', contact: 'Contáctanos' }
|
||||
: { about: 'About', overview: 'Overview', team: 'Team', careers: 'Careers', giveback: 'Giveback', resources: 'Resources', forms: 'Forms', insurance: 'Insurance', referrals: 'Referrals', employee: 'Employee Portal', contact: 'Get In Touch' };
|
||||
const resolve = (label: string, href: string) => {
|
||||
const destination = resolveNavigationDestination(href, lang, manifest);
|
||||
return destination ? { label, ...destination } : null;
|
||||
};
|
||||
const aboutLinks = [
|
||||
resolve(labels.overview, '/about'), resolve(labels.team, '/team'),
|
||||
resolve(labels.careers, '/careers'), resolve(labels.giveback, '/donate-autism-giveback')
|
||||
].filter((item) => item !== null);
|
||||
const resourceLinks = [
|
||||
resolve(labels.forms, '/client-forms'), resolve(labels.insurance, '/insurance'),
|
||||
resolve(labels.referrals, '/referrals'), resolve(labels.employee, '/employee-portal')
|
||||
].filter((item) => item !== null);
|
||||
const privacy = resolve('Privacy Policy', '/privacy-policy');
|
||||
const faq = resolve('FAQ', '/faqs');
|
||||
const sourceLanguage = lang === 'ar' ? 'en' : undefined;
|
||||
---
|
||||
<footer class="site-footer">
|
||||
<div class="container footer-grid">
|
||||
<div>
|
||||
<div lang={sourceLanguage}>
|
||||
<h2>{labels.about}</h2>
|
||||
<ul>
|
||||
<li><a href="/about">{labels.overview}</a></li>
|
||||
<li><a href="/team">{labels.team}</a></li>
|
||||
<li><a href="/careers">{labels.careers}</a></li>
|
||||
<li><a href="/donate-autism-giveback">{labels.giveback}</a></li>
|
||||
{aboutLinks.map((item) => <li><a href={item.href} hreflang={item.fallback ? item.lang : undefined} aria-label={item.fallback ? `${item.label} — ${fallbackLabel(item.lang)}` : undefined}>{item.label}{item.fallback && <bdi class="footer-fallback" lang={item.lang}>{fallbackLabel(item.lang)}</bdi>}</a></li>)}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<div lang={sourceLanguage}>
|
||||
<h2>{labels.resources}</h2>
|
||||
<ul>
|
||||
<li><a href="/client-forms">{labels.forms}</a></li>
|
||||
<li><a href="/insurance">{labels.insurance}</a></li>
|
||||
<li><a href="/referrals">{labels.referrals}</a></li>
|
||||
<li><a href="/employee-portal">{labels.employee}</a></li>
|
||||
{resourceLinks.map((item) => <li><a href={item.href} hreflang={item.fallback ? item.lang : undefined} aria-label={item.fallback ? `${item.label} — ${fallbackLabel(item.lang)}` : undefined}>{item.label}{item.fallback && <bdi class="footer-fallback" lang={item.lang}>{fallbackLabel(item.lang)}</bdi>}</a></li>)}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<div lang={sourceLanguage}>
|
||||
<h2>{labels.contact}</h2>
|
||||
<div class="footer-contact">
|
||||
<div class="footer-contact-item">
|
||||
<img src="/assets/images/phone-blue.svg" alt="" aria-hidden="true" width="24" height="24" />
|
||||
<ContactObfuscation phone={site.phone} hrefValue="+14806877099" obfuscatedText="9907-786 (084)" />
|
||||
<bdi class="footer-contact-value" dir="ltr"><ContactObfuscation phone={site.phone} hrefValue="+14806877099" obfuscatedText="9907-786 (084)" /></bdi>
|
||||
</div>
|
||||
<div class="footer-contact-item">
|
||||
<img src="/assets/images/envelope-blue.svg" alt="" aria-hidden="true" width="24" height="24" />
|
||||
<ContactObfuscation email={site.email} obfuscatedText="moc.msitua4etutitsniza@ofni" />
|
||||
<bdi class="footer-contact-value" dir="ltr"><ContactObfuscation email={site.email} obfuscatedText="moc.msitua4etutitsniza@ofni" /></bdi>
|
||||
</div>
|
||||
<div class="footer-contact-item">
|
||||
<img src="/assets/images/location-blue.svg" alt="" aria-hidden="true" width="24" height="24" />
|
||||
<address>{site.address}</address>
|
||||
<address class="footer-contact-value" dir="ltr">
|
||||
<bdi dir="ltr">
|
||||
{site.address.line1}<br />
|
||||
{site.address.line2}
|
||||
</bdi>
|
||||
</address>
|
||||
</div>
|
||||
</div>
|
||||
<div class="social-links">
|
||||
@@ -61,7 +77,7 @@ const labels = lang === 'es'
|
||||
</div>
|
||||
<div class="container footer-bottom">
|
||||
<p>© 2018-{year} Arizona Institute for Autism ♥ All Rights Reserved</p>
|
||||
<p><a href="/privacy-policy">Privacy Policy</a> | <a href="/faqs">FAQ</a></p>
|
||||
<p lang={sourceLanguage}>{privacy && <a href={privacy.href} hreflang={privacy.fallback ? privacy.lang : undefined} aria-label={privacy.fallback ? `Privacy Policy — ${fallbackLabel(privacy.lang)}` : undefined}>Privacy Policy{privacy.fallback && <bdi class="footer-fallback" lang={privacy.lang}>{fallbackLabel(privacy.lang)}</bdi>}</a>} | {faq && <a href={faq.href} hreflang={faq.fallback ? faq.lang : undefined} aria-label={faq.fallback ? `FAQ — ${fallbackLabel(faq.lang)}` : undefined}>FAQ{faq.fallback && <bdi class="footer-fallback" lang={faq.lang}>{fallbackLabel(faq.lang)}</bdi>}</a>}</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -71,6 +87,8 @@ const labels = lang === 'es'
|
||||
.site-footer h2, .site-footer a { color: white; text-decoration: none; }
|
||||
.site-footer h2 { font-size: 1.25rem; letter-spacing: .3px; margin-bottom: 1.2rem; }
|
||||
.site-footer a:hover { color: var(--color-accent); }
|
||||
.site-footer a:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 3px; }
|
||||
.footer-fallback { border: 1px solid currentColor; border-radius: 2px; font-size: .58rem; line-height: 1; margin-inline-start: .35rem; padding: .1rem .2rem; text-transform: uppercase; }
|
||||
.site-footer ul { list-style: none; padding: 0; }
|
||||
.site-footer li { line-height: 2.6; margin-bottom: 0; }
|
||||
.site-footer p { line-height: 2.6; }
|
||||
@@ -78,6 +96,7 @@ const labels = lang === 'es'
|
||||
.footer-contact-item { align-items: flex-start; display: flex; gap: .6rem; line-height: 1.5; }
|
||||
.footer-contact-item img { flex-shrink: 0; }
|
||||
.footer-contact-item address { font-style: normal; }
|
||||
.footer-contact-value { direction: ltr; text-align: left; unicode-bidi: isolate; }
|
||||
.social-links { display: flex; gap: .85rem; margin-left: 1.6rem; margin-top: .25rem; }
|
||||
.social-links a { align-items: center; color: white; display: flex; opacity: .85; }
|
||||
.social-links a:hover { color: var(--color-accent); opacity: 1; }
|
||||
|
||||
@@ -3,10 +3,12 @@ import Button from './Button.astro';
|
||||
import LanguageSwitcher from './LanguageSwitcher.astro';
|
||||
import MainNav from './MainNav.astro';
|
||||
import MobileNav from './MobileNav.astro';
|
||||
import { getPublicationManifest } from '../utils/publication';
|
||||
const { lang = 'en' } = Astro.props;
|
||||
const localizedHome = lang === 'en' ? '/' : `/${lang}`;
|
||||
const homeHref = (await getPublicationManifest()).some((page) => page.eligible && page.route === localizedHome) ? localizedHome : '/';
|
||||
import { fallbackLabel, resolveNavigationDestination, type VisitorLocale, type VisitorPublication } from '../utils/visitor-navigation';
|
||||
interface Props { lang?: VisitorLocale; publication: VisitorPublication; manifest: VisitorPublication[]; }
|
||||
const { lang = 'en', publication, manifest } = Astro.props;
|
||||
const homeTarget = resolveNavigationDestination(lang === 'en' ? '/' : `/${lang}`, lang, manifest)
|
||||
?? resolveNavigationDestination('/', lang, manifest);
|
||||
const tourTarget = resolveNavigationDestination('/tour', lang, manifest);
|
||||
const utility = lang === 'es'
|
||||
? { credit: 'Solicitar CareCredit', bill: 'Pagar mi factura' }
|
||||
: { credit: 'Apply Care Credit', bill: 'Pay My Bill' };
|
||||
@@ -19,13 +21,13 @@ const utility = lang === 'es'
|
||||
</div>
|
||||
</div>
|
||||
<div class="container header-inner">
|
||||
<a class="brand" href={homeHref}>
|
||||
{homeTarget ? <a class="brand" href={homeTarget.href} lang={homeTarget.fallback ? homeTarget.lang : undefined} hreflang={homeTarget.fallback ? homeTarget.lang : undefined}>
|
||||
<img src="/assets/images/aia-logo.svg" alt="Arizona Institute for Autism" width="150" height="70" />
|
||||
</a>
|
||||
<MainNav lang={lang} />
|
||||
<MobileNav lang={lang} />
|
||||
<LanguageSwitcher />
|
||||
<Button href="/tour" size="small">Tour AIA</Button>
|
||||
</a> : <span class="brand"><img src="/assets/images/aia-logo.svg" alt="Arizona Institute for Autism" width="150" height="70" /></span>}
|
||||
<MainNav lang={lang} manifest={manifest} />
|
||||
<MobileNav lang={lang} manifest={manifest} />
|
||||
<LanguageSwitcher publication={publication} manifest={manifest} />
|
||||
{tourTarget && <Button href={tourTarget.href} size="small">Tour AIA{tourTarget.fallback && ` (${fallbackLabel(tourTarget.lang)})`}</Button>}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
@@ -1,45 +1,57 @@
|
||||
---
|
||||
import { getPublication } from '../utils/publication';
|
||||
const publication = await getPublication(Astro.url.pathname);
|
||||
import { visitorLanguageChoices, type VisitorPublication } from '../utils/visitor-navigation';
|
||||
interface Props { publication: VisitorPublication; manifest: VisitorPublication[]; }
|
||||
const { publication, manifest } = Astro.props;
|
||||
const pathname = publication.route;
|
||||
|
||||
const langMeta: Record<string, { short: string; full: string }> = {
|
||||
en: { short: 'EN', full: 'English' },
|
||||
es: { short: 'ES', full: 'Español' },
|
||||
ar: { short: 'ع', full: 'عربي' },
|
||||
};
|
||||
|
||||
const choices = publication.translations.map(({ lang, route }) => ({ code: lang, href: route }));
|
||||
|
||||
const activeCode = publication.entry.data.lang;
|
||||
const activeShort = langMeta[activeCode]?.short ?? 'EN';
|
||||
const langMeta = {
|
||||
en: { short: 'EN', full: 'English', navigation: 'Language', section: 'section home' },
|
||||
es: { short: 'ES', full: 'Español', navigation: 'Idioma', section: 'inicio de la sección' },
|
||||
ar: { short: 'ع', full: 'العربية', navigation: 'اللغة', section: 'الصفحة الرئيسية للقسم' }
|
||||
} as const;
|
||||
const choices = visitorLanguageChoices(publication, manifest);
|
||||
const activeCode = publication.entry.data.lang as keyof typeof langMeta;
|
||||
const active = langMeta[activeCode] ?? langMeta.en;
|
||||
const hasAlternatives = choices.some(({ route }) => route !== pathname);
|
||||
---
|
||||
<nav class="lang-switcher" aria-label="Language">
|
||||
<div class="lang-switcher__trigger" tabindex={choices.length ? 0 : undefined} aria-haspopup={choices.length ? 'listbox' : undefined} aria-label={`Language: ${activeCode.toUpperCase()}`}>
|
||||
<span class="lang-switcher__label">{activeShort}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855A7.97 7.97 0 0 0 5.145 4H7.5V1.077zM4.09 4a9.267 9.267 0 0 1 .64-1.539 6.7 6.7 0 0 1 .597-.933A7.025 7.025 0 0 0 2.255 4H4.09zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a6.958 6.958 0 0 0-.656 2.5h2.49zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5H4.847zM8.5 5v2.5h2.99a12.495 12.495 0 0 0-.337-2.5H8.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5H4.51zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5H8.5zM5.145 12c.138.386.295.744.468 1.068.552 1.035 1.218 1.65 1.887 1.855V12H5.145zm.182 2.472a6.696 6.696 0 0 1-.597-.933A9.268 9.268 0 0 1 4.09 12H2.255a7.024 7.024 0 0 0 3.072 2.472zM3.82 11a13.652 13.652 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5H3.82zm6.853 3.472A7.024 7.024 0 0 0 13.745 12H11.91a9.27 9.27 0 0 1-.64 1.539 6.688 6.688 0 0 1-.597.933zM8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855.173-.324.33-.682.468-1.068H8.5zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.65 13.65 0 0 1-.312 2.5zm2.802-3.5a6.959 6.959 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5h2.49zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7.024 7.024 0 0 0-3.072-2.472c.218.284.418.598.597.933zM10.855 4a7.966 7.966 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4h2.355z" />
|
||||
</svg>
|
||||
</div>
|
||||
{choices.length > 0 && <ul class="lang-switcher__menu" role="listbox">
|
||||
{choices.map(({ code, href }) => (
|
||||
<li>
|
||||
<a href={href} lang={code} aria-current={pathname === href || (href === '/' && pathname === '/') ? 'page' : undefined}>
|
||||
{langMeta[code]?.full}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>}
|
||||
<nav class="lang-switcher" aria-label={active.navigation}>
|
||||
{hasAlternatives ? (
|
||||
<details class="lang-switcher__details">
|
||||
<summary aria-label={`${active.navigation}: ${active.full}`}>
|
||||
<span class="lang-switcher__label">{active.short}</span>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" aria-hidden="true">
|
||||
<path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855A7.97 7.97 0 0 0 5.145 4H7.5V1.077zM4.09 4a9.267 9.267 0 0 1 .64-1.539 6.7 6.7 0 0 1 .597-.933A7.025 7.025 0 0 0 2.255 4H4.09zm-.582 3.5c.03-.877.138-1.718.312-2.5H1.674a6.958 6.958 0 0 0-.656 2.5h2.49zM4.847 5a12.5 12.5 0 0 0-.338 2.5H7.5V5H4.847zM8.5 5v2.5h2.99a12.495 12.495 0 0 0-.337-2.5H8.5zM4.51 8.5a12.5 12.5 0 0 0 .337 2.5H7.5V8.5H4.51zm3.99 0V11h2.653c.187-.765.306-1.608.338-2.5H8.5zM5.145 12c.138.386.295.744.468 1.068.552 1.035 1.218 1.65 1.887 1.855V12H5.145zm.182 2.472a6.696 6.696 0 0 1-.597-.933A9.268 9.268 0 0 1 4.09 12H2.255a7.024 7.024 0 0 0 3.072 2.472zM3.82 11a13.652 13.652 0 0 1-.312-2.5h-2.49c.062.89.291 1.733.656 2.5H3.82zm6.853 3.472A7.024 7.024 0 0 0 13.745 12H11.91a9.27 9.27 0 0 1-.64 1.539 6.688 6.688 0 0 1-.597.933zM8.5 12v2.923c.67-.204 1.335-.82 1.887-1.855.173-.324.33-.682.468-1.068H8.5zm3.68-1h2.146c.365-.767.594-1.61.656-2.5h-2.49a13.65 13.65 0 0 1-.312 2.5zm2.802-3.5a6.959 6.959 0 0 0-.656-2.5H12.18c.174.782.282 1.623.312 2.5h2.49zM11.27 2.461c.247.464.462.98.64 1.539h1.835a7.024 7.024 0 0 0-3.072-2.472c.218.284.418.598.597.933zM10.855 4a7.966 7.966 0 0 0-.468-1.068C9.835 1.897 9.17 1.282 8.5 1.077V4h2.355z" />
|
||||
</svg>
|
||||
</summary>
|
||||
<ul class="lang-switcher__menu">
|
||||
{choices.map((choice) => {
|
||||
const meta = langMeta[choice.lang];
|
||||
return <li><a href={choice.route} lang={choice.lang} hreflang={choice.lang} aria-current={choice.current ? (choice.route === pathname ? 'page' : 'true') : undefined}><span>{meta.full}</span>{choice.kind === 'section-root' && <small lang={activeCode}>{active.section}</small>}</a></li>;
|
||||
})}
|
||||
</ul>
|
||||
</details>
|
||||
) : <span class="lang-switcher__static" aria-label={`${active.navigation}: ${active.full}`}><span class="lang-switcher__label">{active.short}</span></span>}
|
||||
</nav>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll<HTMLDetailsElement>('.lang-switcher__details').forEach((details) => {
|
||||
details.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape' && details.open) {
|
||||
details.open = false;
|
||||
details.querySelector<HTMLElement>('summary')?.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
.lang-switcher { position: relative; }
|
||||
.lang-switcher__trigger { align-items: center; color: var(--color-primary); cursor: pointer; display: flex; font-size: .85rem; font-weight: 700; gap: .35rem; }
|
||||
.lang-switcher__trigger svg { flex-shrink: 0; }
|
||||
.lang-switcher__menu { background: white; box-shadow: var(--shadow-md); display: none; left: 50%; list-style: none; margin: 0; min-width: 110px; padding: .5rem 0 .4rem; position: absolute; top: 100%; transform: translateX(-50%); z-index: 10; }
|
||||
.lang-switcher__menu a { color: rgb(83 83 91); display: block; font-size: .85rem; padding: .4rem .9rem; text-decoration: none; white-space: nowrap; }
|
||||
.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 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); font-weight: 700; }
|
||||
.lang-switcher:hover .lang-switcher__menu,
|
||||
.lang-switcher:focus-within .lang-switcher__menu { display: block; }
|
||||
.lang-switcher__menu a[aria-current="page"] { color: var(--color-accent-strong); font-weight: 700; }
|
||||
.lang-switcher__menu small { font-size: .68rem; font-weight: 400; }
|
||||
</style>
|
||||
@@ -1,37 +1,51 @@
|
||||
---
|
||||
import en from '../data/navigation.en.json';
|
||||
import es from '../data/navigation.es.json';
|
||||
const { lang = 'en' } = Astro.props;
|
||||
const nav = { en, es, ar: en }[lang as 'en' | 'es' | 'ar'] || en;
|
||||
import ar from '../data/navigation.ar.json';
|
||||
import { fallbackLabel, resolveNavigationDestination, type VisitorLocale, type VisitorPublication } from '../utils/visitor-navigation';
|
||||
interface Props { lang?: VisitorLocale; manifest: VisitorPublication[]; }
|
||||
const { lang = 'en', manifest } = Astro.props;
|
||||
const nav = { en, es, ar }[lang];
|
||||
const aboutLinks = lang === 'es'
|
||||
? [['Equipo', '/team'], ['Carreras', '/careers'], ['Preguntas frecuentes', '/faqs']]
|
||||
: [['Team', '/team'], ['Careers', '/careers'], ['FAQ', '/faqs']];
|
||||
const resolve = (item: { label: string; href: string }) => {
|
||||
const destination = resolveNavigationDestination(item.href, lang, manifest);
|
||||
return destination ? { ...item, ...destination } : null;
|
||||
};
|
||||
const primary = nav.primary.map(resolve).filter((item) => item !== null);
|
||||
const services = nav.services.map(resolve).filter((item) => item !== null);
|
||||
const about = aboutLinks.map(([label, href]) => resolve({ label, href })).filter((item) => item !== null);
|
||||
const navLabel = { en: 'Main navigation', es: 'Navegación principal', ar: 'التنقل الرئيسي' }[lang];
|
||||
---
|
||||
<nav class="main-nav" aria-label="Main navigation">
|
||||
<nav class="main-nav" aria-label={navLabel}>
|
||||
<ul>
|
||||
{nav.primary.map((item) => (
|
||||
{primary.map((item) => (
|
||||
<li class:list={{ 'has-submenu': item.href.endsWith('/about') || item.href.endsWith('/services') }}>
|
||||
<a href={item.href}>{item.label}</a>
|
||||
{item.href.endsWith('/about') && <ul class="submenu">
|
||||
{aboutLinks.map(([label, href]) => <li><a href={href}>{label}</a></li>)}
|
||||
<a href={item.href} hreflang={item.fallback ? item.lang : undefined} aria-label={item.fallback ? `${item.label} — ${fallbackLabel(item.lang)}` : undefined}>
|
||||
{item.label}{item.fallback && <bdi class="nav-fallback" lang={item.lang}>{fallbackLabel(item.lang)}</bdi>}
|
||||
</a>
|
||||
{item.href.endsWith('/about') && about.length > 0 && <ul class="submenu">
|
||||
{about.map((link) => <li><a href={link.href} hreflang={link.fallback ? link.lang : undefined} aria-label={link.fallback ? `${link.label} — ${fallbackLabel(link.lang)}` : undefined}>{link.label}{link.fallback && <bdi class="nav-fallback" lang={link.lang}>{fallbackLabel(link.lang)}</bdi>}</a></li>)}
|
||||
</ul>}
|
||||
{item.href.endsWith('/services') && <ul class="submenu">
|
||||
{nav.services.map((service) => <li><a href={service.href}>{service.label}</a></li>)}
|
||||
{item.href.endsWith('/services') && services.length > 0 && <ul class="submenu">
|
||||
{services.map((service) => <li><a href={service.href} hreflang={service.fallback ? service.lang : undefined} aria-label={service.fallback ? `${service.label} — ${fallbackLabel(service.lang)}` : undefined}>{service.label}{service.fallback && <bdi class="nav-fallback" lang={service.lang}>{fallbackLabel(service.lang)}</bdi>}</a></li>)}
|
||||
</ul>}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
.main-nav { margin-inline: auto; }
|
||||
.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 .submenu { background: white; box-shadow: var(--shadow-md); display: none; gap: 0; left: -1rem; min-width: 180px; padding: .75rem 0; position: absolute; top: 100%; }
|
||||
.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; }
|
||||
@media (max-width: 950px) {
|
||||
.main-nav { display: none; }
|
||||
:global(.mobile-nav) .main-nav { display: block; }
|
||||
|
||||
@@ -1,14 +1,29 @@
|
||||
---
|
||||
import MainNav from './MainNav.astro';
|
||||
const { lang = 'en' } = Astro.props;
|
||||
import type { VisitorLocale, VisitorPublication } from '../utils/visitor-navigation';
|
||||
interface Props { lang?: VisitorLocale; manifest: VisitorPublication[]; }
|
||||
const { lang = 'en', manifest } = Astro.props;
|
||||
const menu = { en: 'Menu', es: 'Menú', ar: 'القائمة' }[lang];
|
||||
---
|
||||
<details class="mobile-nav">
|
||||
<summary>Menu</summary>
|
||||
<MainNav lang={lang} />
|
||||
<summary>{menu}</summary>
|
||||
<MainNav lang={lang} manifest={manifest} />
|
||||
</details>
|
||||
|
||||
<script>
|
||||
document.querySelectorAll<HTMLDetailsElement>('details.mobile-nav').forEach((details) => {
|
||||
details.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape' && details.open) {
|
||||
details.open = false;
|
||||
details.querySelector<HTMLElement>('summary')?.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.mobile-nav { display: none; }
|
||||
.mobile-nav summary:focus-visible { outline: 3px solid var(--color-accent); outline-offset: 3px; }
|
||||
@media (max-width: 950px) {
|
||||
.mobile-nav { display: block; }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user