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
+5
View File
@@ -20,6 +20,11 @@ export interface RevealOptions {
}
export function reveal(node: HTMLElement, options: RevealOptions = {}) {
if (node.closest("[data-admin-preview='true']")) {
node.classList.add("reveal", "revealed")
return
}
if (typeof IntersectionObserver === "undefined") return
const { delay = 0, threshold = 0.15, once = true } = options
+44 -18
View File
@@ -16,6 +16,47 @@ export { locale, isLoading, addMessages } from "svelte-i18n"
register("de", () => import("./locales/de.json"))
register("en", () => import("./locales/en.json"))
let isI18nInitialized = false
let syncSubscriptionsInitialized = false
function ensureI18nInitialized(initialLocale: SupportedLanguage = DEFAULT_LANGUAGE): void {
if (isI18nInitialized) {
return
}
init({
fallbackLocale: DEFAULT_LANGUAGE,
initialLocale,
})
isI18nInitialized = true
}
function ensureLocaleSync(): void {
if (syncSubscriptionsInitialized) {
return
}
// Keep svelte-i18n locale and selectedLanguage store in sync
locale.subscribe((newLocale) => {
if (newLocale && SUPPORTED_LANGUAGES.includes(newLocale as SupportedLanguage)) {
selectedLanguage.set(newLocale as SupportedLanguage)
}
})
selectedLanguage.subscribe((newLang) => {
const currentLocale = get(locale)
if (newLang && newLang !== currentLocale) {
locale.set(newLang)
}
})
syncSubscriptionsInitialized = true
}
ensureI18nInitialized()
ensureLocaleSync()
/**
* Determine the initial locale from URL, browser, or fallback.
*/
@@ -50,26 +91,11 @@ function getInitialLocale(url?: string): SupportedLanguage {
export async function setupI18n(url?: string): Promise<void> {
const initialLocale = getInitialLocale(url)
init({
fallbackLocale: DEFAULT_LANGUAGE,
initialLocale,
})
ensureI18nInitialized(initialLocale)
ensureLocaleSync()
selectedLanguage.set(initialLocale)
// Keep svelte-i18n locale and selectedLanguage store in sync
locale.subscribe((newLocale) => {
if (newLocale && SUPPORTED_LANGUAGES.includes(newLocale as SupportedLanguage)) {
selectedLanguage.set(newLocale as SupportedLanguage)
}
})
selectedLanguage.subscribe((newLang) => {
const currentLocale = get(locale)
if (newLang && newLang !== currentLocale) {
locale.set(newLang)
}
})
locale.set(initialLocale)
await waitLocale()
}
+1
View File
@@ -50,6 +50,7 @@
},
"welcome": "Willkommen",
"language": "Sprache",
"skipToMainContent": "Zum Hauptinhalt springen",
"scrollToTop": "Nach oben",
"loading": "Laden…"
}
+1
View File
@@ -50,6 +50,7 @@
},
"welcome": "Welcome",
"language": "Language",
"skipToMainContent": "Skip to main content",
"scrollToTop": "Scroll to top",
"loading": "Loading…"
}
+123 -5
View File
@@ -14,11 +14,50 @@
// Add new collections here as needed.
// ---------------------------------------------------------------------------
import contentData from "../../mocking/content.json"
import medialibData from "../../mocking/medialib.json"
import navigationData from "../../mocking/navigation.json"
type EJsonObjectId = {
$oid: string
}
const mockRegistry: Record<string, Record<string, unknown>[]> = {
content: contentData as Record<string, unknown>[],
navigation: navigationData as Record<string, unknown>[],
content: normalizeMockCollection(contentData as Record<string, unknown>[]),
medialib: normalizeMockCollection(medialibData as Record<string, unknown>[]),
navigation: normalizeMockCollection(navigationData as Record<string, unknown>[]),
}
function isEJsonObjectId(value: unknown): value is EJsonObjectId {
return !!value && typeof value === "object" && "$oid" in value && typeof (value as EJsonObjectId).$oid === "string"
}
function normalizeMockCollection(entries: Record<string, unknown>[]): Record<string, unknown>[] {
return entries.map((entry) => normalizeMockValue(entry))
}
function normalizeMockValue<T>(value: T): T {
if (Array.isArray(value)) {
return value.map((item) => normalizeMockValue(item)) as T
}
if (!value || typeof value !== "object") {
return value
}
const normalized: Record<string, unknown> = {}
for (const [key, nestedValue] of Object.entries(value as Record<string, unknown>)) {
normalized[key] = normalizeMockValue(nestedValue)
}
if (isEJsonObjectId(normalized._id)) {
normalized._id = normalized._id.$oid
}
if (typeof normalized._id === "string" && normalized.id === undefined) {
normalized.id = normalized._id
}
return normalized as T
}
// ---------------------------------------------------------------------------
@@ -49,9 +88,10 @@ export function mockApiRequest(endpoint: string, options?: ApiOptions, _body?: u
// --- Single-item retrieval ---
if (itemId) {
const item = sourceData.find((e) => e.id === itemId || e._id === itemId)
const resultItem = item ? applyLookups(cloneEntry(item), options) : null
return {
data: item ?? null,
count: item ? 1 : 0,
data: resultItem,
count: resultItem ? 1 : 0,
buildTime: null,
}
}
@@ -79,6 +119,8 @@ export function mockApiRequest(endpoint: string, options?: ApiOptions, _body?: u
results = results.slice(0, options.limit)
}
results = results.map((entry) => applyLookups(cloneEntry(entry), options))
// Projection
if (options?.projection) {
results = applyProjection(results, options.projection)
@@ -184,6 +226,81 @@ function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
}, obj)
}
function cloneEntry<T>(entry: T): T {
return JSON.parse(JSON.stringify(entry)) as T
}
function applyLookups(entry: Record<string, unknown>, options?: ApiOptions): Record<string, unknown> {
const lookupSpecs = parseLookupSpecs(options)
if (!lookupSpecs.length) return entry
for (const spec of lookupSpecs) {
const [fieldPath, collection] = spec.split(":")
if (!fieldPath || !collection) continue
const lookupSource = mockRegistry[collection]
if (!lookupSource) continue
applyLookupAtPath(entry, fieldPath.split("."), lookupSource)
}
return entry
}
function parseLookupSpecs(options?: ApiOptions): string[] {
const rawLookup = [options?.lookup, options?.params?.lookup]
.filter((value): value is string => typeof value === "string" && value.length > 0)
.flatMap((value) => value.split(","))
.map((value) => value.trim())
.filter(Boolean)
return Array.from(new Set(rawLookup))
}
function applyLookupAtPath(
current: Record<string, unknown>,
pathSegments: string[],
lookupSource: Record<string, unknown>[]
): void {
const [segment, ...rest] = pathSegments
if (!segment) return
const value = current[segment]
if (rest.length === 0) {
current._lookup = (current._lookup as Record<string, unknown> | undefined) || {}
;(current._lookup as Record<string, unknown>)[segment] = resolveLookupValue(value, lookupSource)
return
}
if (Array.isArray(value)) {
value.forEach((item) => {
if (item && typeof item === "object") {
applyLookupAtPath(item as Record<string, unknown>, rest, lookupSource)
}
})
return
}
if (value && typeof value === "object") {
applyLookupAtPath(value as Record<string, unknown>, rest, lookupSource)
}
}
function resolveLookupValue(value: unknown, lookupSource: Record<string, unknown>[]): unknown {
if (Array.isArray(value)) {
return value.map((entryId) => resolveLookupById(entryId, lookupSource))
}
return resolveLookupById(value, lookupSource)
}
function resolveLookupById(value: unknown, lookupSource: Record<string, unknown>[]): Record<string, unknown> | null {
if (typeof value !== "string") return null
return lookupSource.find((entry) => entry.id === value || entry._id === value) || null
}
// ---------------------------------------------------------------------------
// Sort
// ---------------------------------------------------------------------------
@@ -244,7 +361,8 @@ function applyProjection(data: Record<string, unknown>[], projectionStr: string)
if (field in entry) result[field] = entry[field]
}
// Always include id/_id
if (entry.id !== undefined) result.id = entry.id
if (typeof entry.id === "string") result.id = entry.id
else if (typeof entry._id === "string") result.id = entry._id
if (entry._id !== undefined) result._id = entry._id
return result
}