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:
2026-05-12 13:55:32 +00:00
parent 8fb26fdeba
commit e84b87ed16
41 changed files with 1523 additions and 338 deletions
+23 -77
View File
@@ -1,47 +1,10 @@
<script lang="ts">
import { getDBEntries, getDBEntry } from "../lib/api"
import { apiBaseURL } from "../config"
import { apiBaseOverride } from "../lib/store"
import { get } from "svelte/store"
// Medialib cache (module-level)
const medialibCache: { [id: string]: MedialibEntry } = {}
let loadQueue: string[] = []
let debounceTimer: ReturnType<typeof setTimeout> | null = null
async function processQueue() {
if (loadQueue.length) {
const _ids = [...loadQueue]
loadQueue = []
const entries = await getDBEntries(
"medialib",
{ _id: { $in: _ids } },
"_id",
undefined,
undefined,
"public"
)
entries.forEach((entry: MedialibEntry) => {
if (entry.id) medialibCache[entry.id] = entry
})
}
}
async function loadMedialibEntry(id: string): Promise<MedialibEntry> {
if (medialibCache[id]) return medialibCache[id]
loadQueue.push(id)
await new Promise<void>((resolve) => {
if (debounceTimer) clearTimeout(debounceTimer)
debounceTimer = setTimeout(async () => {
await processQueue()
resolve()
}, 50)
})
return medialibCache[id]
}
import { currentLanguage, DEFAULT_LANGUAGE } from "../lib/i18n"
interface Props {
id: string
entry?: MedialibEntry | null
filter?: string | null
noPlaceholder?: boolean
caption?: string
@@ -54,6 +17,7 @@
let {
id,
entry = null,
filter = null,
noPlaceholder = false,
caption = "",
@@ -64,11 +28,10 @@
style = "",
}: Props = $props()
let loading = $state(true)
let entry = $state<MedialibEntry | null>(null)
let fileSrc = $state<string | null>(null)
let imgEl = $state<HTMLImageElement | null>(null)
let currentFilter = $state<string>("l-webp")
const effectiveId = $derived(entry?.id || entry?._id || id || "")
const fileSrc = $derived(resolveFileSrc(entry?.file?.src, entry?.id || entry?._id || effectiveId))
// Sync explicit filter prop reactively
$effect(() => {
@@ -94,34 +57,14 @@
return false
}
async function loadFile() {
if (!id) return
loading = true
entry = null
fileSrc = null
try {
const _apiBase = get(apiBaseOverride) || apiBaseURL
entry =
typeof window !== "undefined"
? await loadMedialibEntry(id)
: await getDBEntry("medialib", { _id: id }, "public")
if (entry?.file?.src) {
fileSrc = _apiBase + "medialib/" + id + "/" + entry.file.src
}
} catch (e) {
console.error(e)
}
loading = false
function resolveFileSrc(src: string | undefined, entryId: string | undefined): string | null {
if (!src) return null
if (/^(https?:)?\/\//.test(src) || src.startsWith("/")) return src
if (!entryId) return null
const normalizedApiBase = apiBaseURL.replace(/\/+$/, "")
return `${normalizedApiBase}/medialib/${entryId}/${src.replace(/^\/+/, "")}`
}
// SSR: fire-and-forget — $effect does NOT run during SSR.
// loadFile() internally checks if id is set.
if (typeof window === "undefined") loadFile()
$effect(() => {
if (id) loadFile()
})
// ResizeObserver: only when no explicit filter and raster image
$effect(() => {
const el = imgEl
@@ -147,21 +90,24 @@
if (filter) return src + `?filter=${filter}`
return src + `?filter=${currentFilter}`
}
function resolveLocalizedText(value: string | LocalizedText | undefined, lang: string): string {
if (!value) return ""
if (typeof value === "string") return value
return value[lang] || value[DEFAULT_LANGUAGE] || Object.values(value).find((entry) => !!entry) || ""
}
</script>
{#if id}
{#if loading}
{#if !noPlaceholder}
<img src="/assets/img/placeholder-image.svg" alt="loading" />
{/if}
{:else if entry && fileSrc}
{#if effectiveId}
{#if entry && fileSrc}
{#if showCaption && caption}
<figure>
<picture>
<img
bind:this={imgEl}
src={getSrc(fileSrc, entry)}
alt={entry.alt || ""}
alt={resolveLocalizedText(entry.alt, $currentLanguage)}
data-entry-id={id}
loading={lazy ? "lazy" : undefined}
{style}
@@ -176,7 +122,7 @@
<img
bind:this={imgEl}
src={getSrc(fileSrc, entry)}
alt={entry.alt || ""}
alt={resolveLocalizedText(entry.alt, $currentLanguage)}
data-entry-id={id}
loading={lazy ? "lazy" : undefined}
{style}
@@ -185,7 +131,7 @@
{/if}
{:else if !noPlaceholder}
<picture>
<img src="/assets/img/placeholder-image.svg" alt="not found" data-entry-id={id} />
<img src="/assets/img/placeholder-image.svg" alt="not found" data-entry-id={effectiveId} />
</picture>
{/if}
{/if}