✨ feat: enhance accessibility with skip to main content button and improve navigation handling
🔧 fix: update navigation href resolution to include localized paths 🆕 feat: add new FeatureIcon component for feature boxes 🎨 style: improve styling for prose elements in richtext blocks 🛠️ refactor: streamline medialib image loading and caching logic 📦 chore: update mock data handling to support new medialib entries 🔄 chore: synchronize i18n initialization and locale management 📝 docs: update video tour descriptions to reflect recent changes
This commit is contained in:
+102
-11
@@ -22,6 +22,9 @@
|
||||
stripLanguageFromPath,
|
||||
} from "./lib/i18n"
|
||||
|
||||
const CONTENT_MEDIA_LOOKUP = ["blocks.heroImage.image:medialib", "blocks.image:medialib"].join(",")
|
||||
const NAVIGATION_CONTENT_LOOKUP = "elements.page:content"
|
||||
|
||||
let { url = "" }: { url?: string } = $props()
|
||||
|
||||
initScrollRestoration()
|
||||
@@ -50,6 +53,48 @@
|
||||
}
|
||||
})
|
||||
|
||||
function focusMainContent(updateHash = false) {
|
||||
if (typeof window === "undefined") {
|
||||
return
|
||||
}
|
||||
|
||||
const mainContent = document.getElementById("main-content")
|
||||
if (!(mainContent instanceof HTMLElement)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (updateHash) {
|
||||
window.history.pushState(window.history.state, "", "#main-content")
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
mainContent.scrollIntoView({ block: "start" })
|
||||
mainContent.focus()
|
||||
})
|
||||
}
|
||||
|
||||
function handleSkipToMainContent() {
|
||||
focusMainContent(true)
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (typeof window === "undefined") {
|
||||
return
|
||||
}
|
||||
|
||||
const handleHashChange = () => {
|
||||
if (window.location.hash === "#main-content") {
|
||||
focusMainContent()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener("hashchange", handleHashChange)
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("hashchange", handleHashChange)
|
||||
}
|
||||
})
|
||||
|
||||
// metrics
|
||||
let oldPath = $state("")
|
||||
$effect(() => {
|
||||
@@ -74,6 +119,16 @@
|
||||
let contentEntry = $state<ContentEntry | null>(null)
|
||||
let headerNav = $state<NavigationEntry | null>(null)
|
||||
let footerNav = $state<NavigationEntry | null>(null)
|
||||
|
||||
function resolveNavigationHref(item: NavigationElement): string {
|
||||
const resolvedPagePath = item._lookup?.page?.path || (item.page?.startsWith("/") ? item.page : "/")
|
||||
const localized = localizedPath(resolvedPagePath || "/")
|
||||
|
||||
if (!item.hash) return localized
|
||||
|
||||
const normalizedHash = item.hash.startsWith("#") ? item.hash : `#${item.hash}`
|
||||
return `${localized}${normalizedHash}`
|
||||
}
|
||||
let loading = $state(true)
|
||||
let notFound = $state(false)
|
||||
|
||||
@@ -103,18 +158,42 @@
|
||||
try {
|
||||
// Load navigation
|
||||
const [headerEntries, footerEntries] = await Promise.all([
|
||||
getCachedEntries<"navigation">("navigation", { type: "header", language: lang }),
|
||||
getCachedEntries<"navigation">("navigation", { type: "footer", language: lang }),
|
||||
getCachedEntries<"navigation">(
|
||||
"navigation",
|
||||
{ type: "header", language: lang },
|
||||
"sort",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ lookup: NAVIGATION_CONTENT_LOOKUP }
|
||||
),
|
||||
getCachedEntries<"navigation">(
|
||||
"navigation",
|
||||
{ type: "footer", language: lang },
|
||||
"sort",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ lookup: NAVIGATION_CONTENT_LOOKUP }
|
||||
),
|
||||
])
|
||||
headerNav = headerEntries[0] || null
|
||||
footerNav = footerEntries[0] || null
|
||||
|
||||
// Load content for current path
|
||||
const contentEntries = await getCachedEntries<"content">("content", {
|
||||
lang,
|
||||
path: routePath,
|
||||
active: true,
|
||||
})
|
||||
const contentEntries = await getCachedEntries<"content">(
|
||||
"content",
|
||||
{
|
||||
lang,
|
||||
path: routePath,
|
||||
active: true,
|
||||
},
|
||||
"sort",
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
{ lookup: CONTENT_MEDIA_LOOKUP }
|
||||
)
|
||||
|
||||
if (contentEntries.length > 0) {
|
||||
contentEntry = contentEntries[0]
|
||||
@@ -157,6 +236,9 @@
|
||||
{#if contentEntry?.meta?.description}
|
||||
<meta name="description" content={contentEntry.meta.description} />
|
||||
{/if}
|
||||
{#if contentEntry?.meta?.keywords?.length}
|
||||
<meta name="keywords" content={contentEntry.meta.keywords.join(", ")} />
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<LoadingBar />
|
||||
@@ -168,6 +250,15 @@
|
||||
? 'bg-white/80 backdrop-blur-lg shadow-sm'
|
||||
: 'bg-transparent'}"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-controls="main-content"
|
||||
onclick={handleSkipToMainContent}
|
||||
class="sr-only absolute left-4 top-4 z-50 rounded bg-brand-600 px-4 py-2 text-sm font-semibold text-white focus:not-sr-only"
|
||||
>
|
||||
{$_("skipToMainContent")}
|
||||
</button>
|
||||
|
||||
<div class="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<!-- Logo -->
|
||||
<a
|
||||
@@ -184,7 +275,7 @@
|
||||
{#if headerNav?.elements}
|
||||
{#each headerNav.elements as item}
|
||||
<a
|
||||
href={localizedPath(item.page || "/")}
|
||||
href={resolveNavigationHref(item)}
|
||||
class="text-sm font-medium transition-colors duration-300 hover:text-brand-500 {scrolled
|
||||
? 'text-gray-700'
|
||||
: 'text-white/90'}"
|
||||
@@ -248,7 +339,7 @@
|
||||
{#if headerNav?.elements}
|
||||
{#each headerNav.elements as item}
|
||||
<a
|
||||
href={localizedPath(item.page || "/")}
|
||||
href={resolveNavigationHref(item)}
|
||||
class="block text-gray-700 text-lg font-medium hover:text-brand-600 py-2"
|
||||
>
|
||||
{item.name}
|
||||
@@ -273,7 +364,7 @@
|
||||
</header>
|
||||
|
||||
<!-- ── Main Content ──────────────────────────────────────────── -->
|
||||
<main>
|
||||
<main id="main-content" tabindex="-1">
|
||||
{#if loading}
|
||||
<div class="min-h-screen flex items-center justify-center">
|
||||
<div class="w-8 h-8 border-4 border-brand-200 border-t-brand-600 rounded-full animate-spin"></div>
|
||||
@@ -319,7 +410,7 @@
|
||||
</a>
|
||||
{:else}
|
||||
<a
|
||||
href={localizedPath(item.page || "/")}
|
||||
href={resolveNavigationHref(item)}
|
||||
class="text-sm hover:text-white transition-colors"
|
||||
>
|
||||
{item.name}
|
||||
|
||||
Reference in New Issue
Block a user