Fix preloader

This commit is contained in:
2026-05-21 20:12:53 +04:00
parent 03fc7cc9d5
commit 73dfd4e750
15 changed files with 333 additions and 676 deletions

View File

@@ -1,29 +1,45 @@
---
// 全局Iconify加载器组件
// 在页面头部加载,确保图标库尽早可用
import { url } from "@utils/url";
export interface Props {
preloadIcons?: string[]; // 需要预加载的图标列表
timeout?: number; // 加载超时时间
retryCount?: number; // 重试次数
preloadIcons?: string[];
}
const { preloadIcons = [], timeout = 10000, retryCount = 3 } = Astro.props;
const { preloadIcons = [] } = Astro.props;
const iconifyScriptUrl = url("/assets/iconify/iconify-icon.min.js");
---
<!-- Iconify图标库加载器 -->
<script define:vars={{ preloadIcons, timeout, retryCount }}>
// 全局图标加载逻辑
(function() {
'use strict';
<div
id="iconify-loader-config"
data-script-url={iconifyScriptUrl}
data-preload-icons={JSON.stringify(preloadIcons)}
hidden
aria-hidden="true"
></div>
<script>
import {
registerSvelteIconifyCollections,
registerWebComponentIconifyCollections,
} from "@/utils/iconify-offline";
const LOAD_TIMEOUT_MS = 3000;
const RETRY_COUNT = 1;
(function () {
"use strict";
// 避免重复加载
if (window.__iconifyLoaderInitialized) {
return;
}
window.__iconifyLoaderInitialized = true;
// 图标加载器类
registerSvelteIconifyCollections();
const configEl = document.getElementById("iconify-loader-config");
const iconifyScriptUrl = configEl?.dataset.scriptUrl ?? "/assets/iconify/iconify-icon.min.js";
const preloadIcons = JSON.parse(configEl?.dataset.preloadIcons ?? "[]");
class IconifyLoader {
constructor() {
this.isLoaded = false;
@@ -34,7 +50,10 @@ const { preloadIcons = [], timeout = 10000, retryCount = 3 } = Astro.props;
}
async load(options = {}) {
const { timeout: loadTimeout = timeout, retryCount: maxRetries = retryCount } = options;
const {
timeout: loadTimeout = LOAD_TIMEOUT_MS,
retryCount: maxRetries = RETRY_COUNT,
} = options;
if (this.isLoaded) {
return Promise.resolve();
@@ -53,61 +72,74 @@ const { preloadIcons = [], timeout = 10000, retryCount = 3 } = Astro.props;
this.notifyObservers();
await this.processPreloadQueue();
} catch (error) {
console.error('Failed to load Iconify:', error);
console.error("Failed to load Iconify:", error);
throw error;
} finally {
this.isLoading = false;
}
}
async loadWithRetry(timeout, retryCount) {
for (let attempt = 1; attempt <= retryCount; attempt++) {
async loadWithRetry(timeoutMs, maxRetries) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await this.loadScript(timeout);
await this.loadScript(timeoutMs);
return;
} catch (error) {
console.warn(`Iconify load attempt ${attempt} failed:`, error);
if (attempt === retryCount) {
throw new Error(`Failed to load Iconify after ${retryCount} attempts`);
if (attempt === maxRetries) {
throw new Error(
`Failed to load Iconify after ${maxRetries} attempts`,
);
}
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 500));
}
}
}
loadScript(timeout) {
loadScript(timeoutMs) {
return new Promise((resolve, reject) => {
// 检查是否已经存在
if (this.isIconifyReady()) {
registerWebComponentIconifyCollections();
resolve();
return;
}
const existingScript = document.querySelector('script[src*="iconify-icon"]');
const existingScript = document.querySelector(
'script[src*="iconify-icon"]',
);
if (existingScript) {
this.waitForIconifyReady().then(resolve).catch(reject);
this.waitForIconifyReady()
.then(() => {
registerWebComponentIconifyCollections();
resolve();
})
.catch(reject);
return;
}
const script = document.createElement('script');
script.src = 'https://code.iconify.design/iconify-icon/1.0.7/iconify-icon.min.js';
const script = document.createElement("script");
script.src = iconifyScriptUrl;
script.async = true;
script.crossOrigin = 'anonymous';
const timeoutId = setTimeout(() => {
script.remove();
reject(new Error('Script load timeout'));
}, timeout);
reject(new Error("Script load timeout"));
}, timeoutMs);
script.onload = () => {
clearTimeout(timeoutId);
this.waitForIconifyReady().then(resolve).catch(reject);
this.waitForIconifyReady()
.then(() => {
registerWebComponentIconifyCollections();
resolve();
})
.catch(reject);
};
script.onerror = () => {
clearTimeout(timeoutId);
script.remove();
reject(new Error('Script load error'));
reject(new Error("Script load error"));
};
document.head.appendChild(script);
@@ -125,7 +157,7 @@ const { preloadIcons = [], timeout = 10000, retryCount = 3 } = Astro.props;
}
if (Date.now() - startTime > maxWait) {
reject(new Error('Iconify initialization timeout'));
reject(new Error("Iconify initialization timeout"));
return;
}
@@ -137,7 +169,11 @@ const { preloadIcons = [], timeout = 10000, retryCount = 3 } = Astro.props;
}
isIconifyReady() {
return typeof window !== 'undefined' && 'customElements' in window && customElements.get('iconify-icon') !== undefined;
return (
typeof window !== "undefined" &&
"customElements" in window &&
customElements.get("iconify-icon") !== undefined
);
}
onLoad(callback) {
@@ -149,11 +185,11 @@ const { preloadIcons = [], timeout = 10000, retryCount = 3 } = Astro.props;
}
notifyObservers() {
this.observers.forEach(callback => {
this.observers.forEach((callback) => {
try {
callback();
} catch (error) {
console.error('Error in icon load observer:', error);
console.error("Error in icon load observer:", error);
}
});
this.observers.clear();
@@ -161,7 +197,7 @@ const { preloadIcons = [], timeout = 10000, retryCount = 3 } = Astro.props;
addToPreloadQueue(icons) {
if (Array.isArray(icons)) {
icons.forEach(icon => this.preloadQueue.add(icon));
icons.forEach((icon) => this.preloadQueue.add(icon));
} else {
this.preloadQueue.add(icons);
}
@@ -172,88 +208,34 @@ const { preloadIcons = [], timeout = 10000, retryCount = 3 } = Astro.props;
}
async processPreloadQueue() {
if (this.preloadQueue.size === 0) return;
const iconsToLoad = Array.from(this.preloadQueue);
this.preloadQueue.clear();
await this.preloadIcons(iconsToLoad);
}
async preloadIcons(icons) {
if (!this.isLoaded || icons.length === 0) return;
return new Promise((resolve) => {
let loadedCount = 0;
const totalIcons = icons.length;
const tempElements = [];
const cleanup = () => {
tempElements.forEach(el => {
if (el.parentNode) {
el.parentNode.removeChild(el);
}
});
};
const checkComplete = () => {
loadedCount++;
if (loadedCount >= totalIcons) {
cleanup();
resolve();
}
};
icons.forEach(icon => {
const tempIcon = document.createElement('iconify-icon');
tempIcon.setAttribute('icon', icon);
tempIcon.style.cssText = 'position:absolute;top:-9999px;left:-9999px;width:1px;height:1px;opacity:0;pointer-events:none;';
tempIcon.addEventListener('load', checkComplete);
tempIcon.addEventListener('error', checkComplete);
tempElements.push(tempIcon);
document.body.appendChild(tempIcon);
});
// 设置超时清理
setTimeout(() => {
cleanup();
resolve();
}, 3000);
});
}
}
// 创建全局实例
window.__iconifyLoader = new IconifyLoader();
// 立即开始加载
window.__iconifyLoader.load().catch(error => {
console.error('Failed to initialize Iconify:', error);
window.__iconifyLoader.load().catch((error) => {
console.error("Failed to initialize Iconify:", error);
});
// 如果有预加载图标,添加到队列
if (preloadIcons && preloadIcons.length > 0) {
if (preloadIcons.length > 0) {
window.__iconifyLoader.addToPreloadQueue(preloadIcons);
}
// 导出便捷函数到全局
window.loadIconify = () => window.__iconifyLoader.load();
window.preloadIcons = (icons) => window.__iconifyLoader.addToPreloadQueue(icons);
window.onIconifyReady = (callback) => window.__iconifyLoader.onLoad(callback);
window.preloadIcons = (icons) =>
window.__iconifyLoader.addToPreloadQueue(icons);
window.onIconifyReady = (callback) =>
window.__iconifyLoader.onLoad(callback);
// 页面可见性变化时重新检查
document.addEventListener('visibilitychange', () => {
document.addEventListener("visibilitychange", () => {
if (!document.hidden && !window.__iconifyLoader.isLoaded) {
window.__iconifyLoader.load().catch(console.error);
}
});
})();
</script>
<!-- 为不支持JavaScript的情况提供备用方案 -->
<noscript>
<style>
iconify-icon {
@@ -263,4 +245,4 @@ const { preloadIcons = [], timeout = 10000, retryCount = 3 } = Astro.props;
display: inline-block;
}
</style>
</noscript>
</noscript>

View File

@@ -9,7 +9,7 @@ interface Props {
const className = Astro.props.class;
---
<div data-pagefind-body class={`prose dark:prose-invert prose-base max-w-none! custom-md ${className}`}>
<div class={`prose dark:prose-invert prose-base max-w-none! custom-md ${className}`}>
<!--<div class="prose dark:prose-invert max-w-none custom-md">-->
<!--<div class="max-w-none custom-md">-->
<slot/>

View File

@@ -8,7 +8,6 @@ import { url } from "@utils/url";
import { getNavbarTransparentModeForWallpaperMode, getDefaultWallpaperMode } from "@utils/wallpaper";
import NavLinks from "@components/navbar/navLinks.astro";
import NavMenu from "@components/navbar/navMenu.svelte";
import Search from "@components/navbar/search.svelte";
import Translator from "@components/navbar/translator.svelte";
import DisplaySettings from "@components/navbar/displaySettings.svelte";
import LightDarkSwitch from "@components/navbar/lightDarkSwitch.svelte";
@@ -51,7 +50,6 @@ let links: NavbarLink[] = navbarConfig.links.map(
})}
</div>
<div class="flex items-center navbar-buttons" id="navbar-buttons">
<!-- <Search client:load></Search> -->
<Translator client:load></Translator>
<!-- <DisplaySettings client:load></DisplaySettings> -->
<LightDarkSwitch client:load></LightDarkSwitch>
@@ -133,61 +131,4 @@ if (document.readyState === 'loading') {
} else {
initSemifullScrollDetection();
}
</script>
{import.meta.env.PROD && <script is:inline define:vars={{scriptUrl: url('/pagefind/pagefind.js')}}>
async function loadPagefind() {
try {
const response = await fetch(scriptUrl, { method: 'HEAD' });
if (!response.ok) {
throw new Error(`Pagefind script not found: ${response.status}`);
}
const pagefind = await import(scriptUrl);
await pagefind.options({
excerptLength: 20
});
window.pagefind = pagefind;
document.dispatchEvent(new CustomEvent('pagefindready'));
console.log('Pagefind loaded and initialized successfully, event dispatched.');
} catch (error) {
console.error('Failed to load Pagefind:', error);
window.pagefind = {
search: () => Promise.resolve({ results: [] }),
options: () => Promise.resolve(),
};
document.dispatchEvent(new CustomEvent('pagefindloaderror'));
console.log('Pagefind load error, event dispatched.');
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', loadPagefind);
} else {
loadPagefind();
}
// Переинициализация Pagefind после навигации Swup
if (typeof window !== 'undefined' && window.swup) {
window.swup.hooks.on('page:view', () => {
// Переинициализируем Pagefind после навигации, если он еще не загружен
if (!window.pagefind || typeof window.pagefind.search !== 'function') {
loadPagefind();
}
});
} else if (typeof window !== 'undefined') {
// Если Swup еще не загружен, ждем события
document.addEventListener('swup:enable', () => {
if (window.swup) {
window.swup.hooks.on('page:view', () => {
if (!window.pagefind || typeof window.pagefind.search !== 'function') {
loadPagefind();
}
});
}
});
}
</script>}
</script>

View File

@@ -1,285 +0,0 @@
<script lang="ts">
import { onMount, onDestroy } from "svelte";
import Icon from "@iconify/svelte";
import type { SearchResult } from "@/global";
import { url } from "@utils/url";
import { navigateToPage } from "@utils/navigation";
import { onClickOutside } from "@utils/widget";
import { i18n } from "@i18n/translation";
import I18nKey from "@i18n/i18nKey";
import DropdownPanel from "@/components/common/DropdownPanel.svelte";
let keywordDesktop = $state("");
let keywordMobile = $state("");
let result: SearchResult[] = $state([]);
let isSearching = $state(false);
let pagefindLoaded = false;
let initialized = $state(false);
let isDesktopSearchExpanded = $state(false);
let debounceTimer: NodeJS.Timeout;
const fakeResult: SearchResult[] = [
{
url: url("/"),
meta: {
title: "This Is a Fake Search Result",
},
excerpt:
"Because the search cannot work in the <mark>dev</mark> environment.",
},
{
url: url("/"),
meta: {
title: "If You Want to Test the Search",
},
excerpt: "Try running <mark>npm build && npm preview</mark> instead.",
},
];
const togglePanel = () => {
const panel = document.getElementById("search-panel");
panel?.classList.toggle("float-panel-closed");
};
const toggleDesktopSearch = () => {
isDesktopSearchExpanded = !isDesktopSearchExpanded;
if (isDesktopSearchExpanded) {
setTimeout(() => {
const input = document.getElementById("search-input-desktop") as HTMLInputElement;
input?.focus();
}, 0);
}
};
const collapseDesktopSearch = () => {
if (!keywordDesktop) {
isDesktopSearchExpanded = false;
}
};
const handleBlur = () => {
// 延迟处理以允许搜索结果的点击事件先于折叠逻辑执行
setTimeout(() => {
isDesktopSearchExpanded = false;
// 仅隐藏面板并折叠,保留搜索关键词和结果以便下次展开时查看
setPanelVisibility(false, true);
}, 200);
};
const setPanelVisibility = (show: boolean, isDesktop: boolean): void => {
const panel = document.getElementById("search-panel");
if (!panel || !isDesktop) return;
if (show) {
panel.classList.remove("float-panel-closed");
} else {
panel.classList.add("float-panel-closed");
}
};
const closeSearchPanel = (): void => {
const panel = document.getElementById("search-panel");
if (panel) {
panel.classList.add("float-panel-closed");
}
// 清空搜索关键词和结果
keywordDesktop = "";
keywordMobile = "";
result = [];
};
const handleResultClick = (event: Event, url: string): void => {
event.preventDefault();
closeSearchPanel();
navigateToPage(url);
};
const search = async (keyword: string, isDesktop: boolean): Promise<void> => {
if (!keyword) {
setPanelVisibility(false, isDesktop);
result = [];
return;
}
if (!initialized) {
return;
}
isSearching = true;
try {
let searchResults: SearchResult[] = [];
if (import.meta.env.PROD && pagefindLoaded && window.pagefind) {
const response = await window.pagefind.search(keyword);
searchResults = await Promise.all(
response.results.map((item) => item.data()),
);
} else if (import.meta.env.DEV) {
searchResults = fakeResult;
} else {
searchResults = [];
console.error("Pagefind is not available in production environment.");
}
result = searchResults;
setPanelVisibility(result.length > 0, isDesktop);
} catch (error) {
console.error("Search error:", error);
result = [];
setPanelVisibility(false, isDesktop);
} finally {
isSearching = false;
}
};
const handleClickOutside = (event: MouseEvent) => {
const panel = document.getElementById("search-panel");
if (!panel || panel.classList.contains("float-panel-closed")) {
return;
}
onClickOutside(event, "search-panel", ["search-switch", "search-bar"], () => {
const panel = document.getElementById("search-panel");
panel?.classList.add("float-panel-closed");
isDesktopSearchExpanded = false;
});
};
onMount(() => {
document.addEventListener("click", handleClickOutside);
const initializeSearch = () => {
initialized = true;
pagefindLoaded =
typeof window !== "undefined" &&
!!window.pagefind &&
typeof window.pagefind.search === "function";
console.log("Pagefind status on init:", pagefindLoaded);
};
if (import.meta.env.DEV) {
console.log(
"Pagefind is not available in development mode. Using mock data.",
);
initializeSearch();
} else {
document.addEventListener("pagefindready", () => {
console.log("Pagefind ready event received.");
initializeSearch();
});
document.addEventListener("pagefindloaderror", () => {
console.warn(
"Pagefind load error event received. Search functionality will be limited.",
);
initializeSearch(); // Initialize with pagefindLoaded as false
});
// Fallback in case events are not caught or pagefind is already loaded by the time this script runs
setTimeout(() => {
if (!initialized) {
console.log("Fallback: Initializing search after timeout.");
initializeSearch();
}
}, 2000); // Adjust timeout as needed
}
});
$effect(() => {
if (initialized) {
const keyword = keywordDesktop || keywordMobile;
const isDesktop = !!keywordDesktop || isDesktopSearchExpanded;
clearTimeout(debounceTimer);
if (keyword) {
debounceTimer = setTimeout(() => {
search(keyword, isDesktop);
}, 300);
} else {
result = [];
setPanelVisibility(false, isDesktop);
}
}
});
$effect(() => {
if (typeof document !== 'undefined') {
const navbar = document.getElementById('navbar');
if (isDesktopSearchExpanded) {
navbar?.classList.add('is-searching');
} else {
navbar?.classList.remove('is-searching');
}
}
});
onDestroy(() => {
if (typeof document !== 'undefined') {
document.removeEventListener("click", handleClickOutside);
const navbar = document.getElementById('navbar');
navbar?.classList.remove('is-searching');
}
clearTimeout(debounceTimer);
});
</script>
<!-- search bar for desktop view (collapsed by default) -->
<div
id="search-bar"
class="hidden lg:flex transition-all items-center h-11 rounded-lg
{isDesktopSearchExpanded ? 'bg-black/4 hover:bg-black/6 focus-within:bg-black/6 dark:bg-white/5 dark:hover:bg-white/10 dark:focus-within:bg-white/10' : 'btn-plain scale-animation active:scale-90'}
{isDesktopSearchExpanded ? 'w-48' : 'w-11'}"
role="button"
tabindex="0"
aria-label="Search"
onmouseenter={() => {if (!isDesktopSearchExpanded) toggleDesktopSearch()}}
onmouseleave={collapseDesktopSearch}
>
<Icon icon="material-symbols:search" class="absolute text-[1.25rem] pointer-events-none {isDesktopSearchExpanded ? 'ml-3' : 'left-1/2 -translate-x-1/2'} transition my-auto {isDesktopSearchExpanded ? 'text-black/30 dark:text-white/30' : ''}"></Icon>
<input id="search-input-desktop" placeholder="{i18n(I18nKey.search)}" bind:value={keywordDesktop}
onfocus={() => {if (!isDesktopSearchExpanded) toggleDesktopSearch(); search(keywordDesktop, true)}}
onblur={handleBlur}
class="transition-all pl-10 text-sm bg-transparent outline-0
h-full {isDesktopSearchExpanded ? 'w-36' : 'w-0'} text-black/50 dark:text-white/50"
>
</div>
<!-- toggle btn for phone/tablet view -->
<button onclick={togglePanel} aria-label="Search Panel" id="search-switch"
class="btn-plain scale-animation lg:hidden! rounded-lg w-11 h-11 active:scale-90">
<Icon icon="material-symbols:search" class="text-[1.25rem]"></Icon>
</button>
<!-- search panel -->
<DropdownPanel
id="search-panel"
class="float-panel-closed absolute md:w-120 top-20 left-4 md:left-[unset] right-4 z-50 search-panel"
>
<!-- search bar inside panel for phone/tablet -->
<div id="search-bar-inside" class="flex relative lg:hidden transition-all items-center h-11 rounded-xl
bg-black/4 hover:bg-black/6 focus-within:bg-black/6
dark:bg-white/5 dark:hover:bg-white/10 dark:focus-within:bg-white/10
">
<Icon icon="material-symbols:search" class="absolute text-[1.25rem] pointer-events-none ml-3 transition my-auto text-black/30 dark:text-white/30"></Icon>
<input placeholder="Search" bind:value={keywordMobile}
class="pl-10 absolute inset-0 text-sm bg-transparent outline-0
focus:w-60 text-black/50 dark:text-white/50"
>
</div>
<!-- search results -->
{#each result as item}
<a href={item.url}
onclick={(e) => handleResultClick(e, item.url)}
class="transition first-of-type:mt-2 lg:first-of-type:mt-0 group block
rounded-xl text-lg px-3 py-2 hover:bg-(--btn-plain-bg-hover) active:bg-(--btn-plain-bg-active)">
<div class="transition text-90 inline-flex font-bold group-hover:text-(--primary)">
{item.meta.title}<Icon icon="fa6-solid:chevron-right" class="transition text-[0.75rem] translate-x-1 my-auto text-(--primary)"></Icon>
</div>
<div class="transition text-sm text-50">
{@html item.excerpt}
</div>
</a>
{/each}
</DropdownPanel>
<style>
input:focus {
outline: 0;
}
:global(.search-panel) {
max-height: calc(100vh - 100px);
overflow-y: auto;
}
</style>

37
src/global.d.ts vendored
View File

@@ -14,13 +14,6 @@ declare global {
__iconifyLoader: {
load: () => Promise<void>;
};
pagefind: {
search: (query: string) => Promise<{
results: Array<{
data: () => Promise<SearchResult>;
}>;
}>;
};
translate?: {
service: {
use: (service: string) => void;
@@ -51,32 +44,4 @@ declare global {
}
}
interface SearchResult {
url: string;
meta: {
title: string;
};
excerpt: string;
content?: string;
word_count?: number;
filters?: Record<string, unknown>;
anchors?: Array<{
element: string;
id: string;
text: string;
location: number;
}>;
weighted_locations?: Array<{
weight: number;
balanced_score: number;
location: number;
}>;
locations?: number[];
raw_content?: string;
raw_url?: string;
sub_results?: SearchResult[];
}
export { SearchResult };
export {};

View File

@@ -127,7 +127,6 @@ const umamiScripts = umamiConfig.scripts || ""; // 获取Umami scripts配置
<!-- Resource Preconnect -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="preconnect" href="https://code.iconify.design">
<!-- Font Loader -->
<FontLoader />
<!-- title -->
@@ -481,42 +480,41 @@ loadAndInitTranslate();
// Initialize particle effects
setupParticleEffects();
// Remove is-loading class and trigger initial-animation after page load
if (typeof window !== 'undefined') {
window.addEventListener('load', () => {
const overlay = document.getElementById('loading-overlay');
const enableBanner = document.body.classList.contains('enable-banner');
// 触发 LoadingOverlay 淡出
function hideLoadingOverlay() {
const overlay = document.getElementById('loading-overlay');
const enableBanner = document.body.classList.contains('enable-banner');
if (overlay) {
overlay.classList.add('fade-out');
}
setTimeout(() => {
if (overlay) {
overlay.classList.add('fade-out');
overlay.style.display = 'none';
}
// 等待 LoadingOverlay 淡出600ms
setTimeout(() => {
if (overlay) {
overlay.style.display = 'none';
}
if (enableBanner) {
// banner 开始恢复过渡:添加 banner-restoring 类(解除 CSS 中的 height: 0
document.documentElement.classList.add('banner-restoring');
// 等待 banner 恢复过渡完成600ms
setTimeout(() => {
// 触发各容器淡入动画
document.documentElement.classList.remove('is-loading');
document.documentElement.classList.add('show-initial-animation');
setTimeout(() => {
document.documentElement.classList.remove('show-initial-animation');
document.documentElement.classList.remove('banner-restoring');
}, 1200);
}, 600);
} else {
// 非 banner 模式,直接触发淡入
if (enableBanner) {
document.documentElement.classList.add('banner-restoring');
setTimeout(() => {
document.documentElement.classList.remove('is-loading');
document.documentElement.classList.add('show-initial-animation');
setTimeout(() => {
document.documentElement.classList.remove('show-initial-animation');
document.documentElement.classList.remove('banner-restoring');
}, 1200);
}
}, 600);
});
}, 600);
} else {
document.documentElement.classList.remove('is-loading');
document.documentElement.classList.add('show-initial-animation');
setTimeout(() => {
document.documentElement.classList.remove('show-initial-animation');
}, 1200);
}
}, 600);
}
if (typeof window !== 'undefined') {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', hideLoadingOverlay);
} else {
hideLoadingOverlay();
}
}
</script>

View File

@@ -153,9 +153,6 @@ const jsonLd = {
<!-- title -->
<div class="relative onload-animation-up">
<div
data-pagefind-body
data-pagefind-weight="10"
data-pagefind-meta="title"
class="transition w-full block font-bold mb-3
text-3xl md:text-[2.25rem]/[2.75rem]
text-black/90 dark:text-white/90

View File

@@ -0,0 +1,78 @@
import { addCollection } from "@iconify/svelte/dist/functions.js";
import type { IconifyJSON } from "@iconify/types";
import devicon from "@iconify-json/devicon/icons.json";
import gameIcons from "@iconify-json/game-icons/icons.json";
import hugeicons from "@iconify-json/hugeicons/icons.json";
import iconoir from "@iconify-json/iconoir/icons.json";
import logos from "@iconify-json/logos/icons.json";
import materialIconTheme from "@iconify-json/material-icon-theme/icons.json";
import materialSymbols from "@iconify-json/material-symbols/icons.json";
import mdi from "@iconify-json/mdi/icons.json";
import mingcute from "@iconify-json/mingcute/icons.json";
import skillIcons from "@iconify-json/skill-icons/icons.json";
import vscodeIcons from "@iconify-json/vscode-icons/icons.json";
const collections: IconifyJSON[] = [
materialSymbols,
logos,
devicon,
hugeicons,
mingcute,
mdi,
skillIcons,
vscodeIcons,
materialIconTheme,
gameIcons,
iconoir,
];
let svelteRegistered = false;
let webComponentRegistered = false;
function addCollectionsToWebComponent(): void {
if (typeof window === "undefined" || webComponentRegistered) {
return;
}
const IconifyIcon = customElements.get("iconify-icon") as
| (CustomElementConstructor & {
addCollection?: (data: IconifyJSON, provider?: string) => boolean;
disableCache?: (storage: string) => void;
})
| undefined;
if (!IconifyIcon?.addCollection) {
return;
}
IconifyIcon.disableCache?.("all");
for (const collection of collections) {
IconifyIcon.addCollection(collection);
}
webComponentRegistered = true;
}
/** Register offline collections for @iconify/svelte (navbar, player, etc.). */
export function registerSvelteIconifyCollections(): void {
if (svelteRegistered) {
return;
}
svelteRegistered = true;
for (const collection of collections) {
addCollection(collection);
}
}
/** Register offline collections for iconify-icon web component (icon.astro). */
export function registerWebComponentIconifyCollections(): void {
addCollectionsToWebComponent();
}
/** Register all offline icon sets (Svelte + web component when available). */
export function registerIconifyCollections(): void {
registerSvelteIconifyCollections();
registerWebComponentIconifyCollections();
}