Cut iconify

This commit is contained in:
2026-05-21 20:59:02 +04:00
parent 73dfd4e750
commit 95342ae758
27 changed files with 733 additions and 686 deletions

View File

@@ -31,6 +31,28 @@ import { parseDirectiveNode } from "./src/plugins/remark-directive-rehype.js";
import { remarkExcerpt } from "./src/plugins/remark-excerpt.js";
import { remarkMermaid } from "./src/plugins/remark-mermaid.js";
import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs";
import { fileURLToPath } from "node:url";
import iconInclude from "./src/generated/icon-include.mjs";
const iconIncludeFile = fileURLToPath(
new URL("./src/generated/icon-include.mjs", import.meta.url),
);
/** Restart dev server when collect-icons updates astro-icon include */
function iconIncludeHmr() {
return {
name: "icon-include-hmr",
configureServer(server) {
server.watcher.add(iconIncludeFile);
server.watcher.on("change", (file) => {
if (file === iconIncludeFile) {
console.log("[icons] icon-include.mjs changed, restarting dev server…");
server.restart();
}
});
},
};
}
// https://astro.build/config
@@ -75,12 +97,7 @@ export default defineConfig({
},
}),
icon({
include: {
"fa6-brands": ["*"],
"fa6-regular": ["*"],
"fa6-solid": ["*"],
mdi: ["*"],
},
include: iconInclude,
}),
expressiveCode({
themes: ["github-light", "github-dark"],
@@ -191,7 +208,7 @@ export default defineConfig({
],
},
vite: {
plugins: [tailwindcss()],
plugins: [tailwindcss(), iconIncludeHmr()],
build: {
rollupOptions: {
onwarn(warning, warn) {

View File

@@ -9,9 +9,10 @@
"lint": "biome check --write ./src",
"format": "biome format --write ./src",
"check": "astro check",
"dev": "astro dev",
"build": "node scripts/copy-iconify.cjs && astro build",
"copy-iconify": "node scripts/copy-iconify.cjs",
"dev": "node scripts/collect-icons.mjs && node scripts/generate-site-icons.mjs && astro dev",
"icons:collect": "node scripts/collect-icons.mjs",
"icons:generate": "node scripts/generate-site-icons.mjs",
"build": "node scripts/collect-icons.mjs && node scripts/generate-site-icons.mjs && astro build",
"preview": "astro preview",
"astro": "astro",
"new-post": "node scripts/new-post.js"
@@ -44,8 +45,6 @@
"@iconify-json/mingcute": "^1.2.5",
"@iconify-json/skill-icons": "^1.2.3",
"@iconify-json/vscode-icons": "^1.2.40",
"@iconify/utils": "^2.3.0",
"@iconify/svelte": "^5.2.1",
"@swup/astro": "^1.7.0",
"@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.18",
@@ -65,7 +64,6 @@
"marked": "^17.0.1",
"mdast-util-to-string": "^4.0.0",
"node-html-parser": "^7.0.2",
"iconify-icon": "1.0.7",
"overlayscrollbars": "^2.14.0",
"photoswipe": "^5.4.4",
"reading-time": "^1.5.0",
@@ -95,7 +93,8 @@
"@types/markdown-it": "^14.1.2",
"@types/mdast": "^4.0.4",
"@types/sanitize-html": "^2.16.0",
"postcss-import": "^16.1.1"
"postcss-import": "^16.1.1",
"@iconify/utils": "^2.3.0"
},
"packageManager": "pnpm@9.14.4"
}

29
pnpm-lock.yaml generated
View File

@@ -89,12 +89,6 @@ importers:
'@iconify-json/vscode-icons':
specifier: ^1.2.40
version: 1.2.50
'@iconify/svelte':
specifier: ^5.2.1
version: 5.2.1(svelte@5.49.1)
'@iconify/utils':
specifier: ^2.3.0
version: 2.3.0
'@swup/astro':
specifier: ^1.7.0
version: 1.7.0
@@ -134,9 +128,6 @@ importers:
hastscript:
specifier: ^9.0.1
version: 9.0.1
iconify-icon:
specifier: 1.0.7
version: 1.0.7
js-yaml:
specifier: ^4.1.1
version: 4.1.1
@@ -222,6 +213,9 @@ importers:
'@biomejs/biome':
specifier: 2.3.13
version: 2.3.13
'@iconify/utils':
specifier: ^2.3.0
version: 2.3.0
'@rollup/plugin-yaml':
specifier: ^4.1.2
version: 4.1.2(rollup@2.79.2)
@@ -1395,11 +1389,6 @@ packages:
'@iconify-json/vscode-icons@1.2.50':
resolution: {integrity: sha512-XqG9nO1m+IiY8/RvT5BJjM/eG3CDRXBGRRPqR/MAcVi734yZ0Pa+jqObshZfnpUeIsuDKNn9C/7ZW00ART6kog==}
'@iconify/svelte@5.2.1':
resolution: {integrity: sha512-zHmsIPmnIhGd5gc95bNN5FL+GifwMZv7M2rlZEpa7IXYGFJm/XGHdWf6PWQa6OBoC+R69WyiPO9NAj5wjfjbow==}
peerDependencies:
svelte: '>5.0.0'
'@iconify/tools@4.2.0':
resolution: {integrity: sha512-WRxPva/ipxYkqZd1+CkEAQmd86dQmrwH0vwK89gmp2Kh2WyyVw57XbPng0NehP3x4V1LzLsXUneP1uMfTMZmUA==}
@@ -3369,9 +3358,6 @@ packages:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
iconify-icon@1.0.7:
resolution: {integrity: sha512-MxaO3Jhf3f5ymPWGHR9x74f90TNKcq1D+B2iGucGhVtqAgbC9EtM06kKiTGH2CKELNnexckwhrA3/+OpT4HKFw==}
iconv-lite@0.6.3:
resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
engines: {node: '>=0.10.0'}
@@ -6949,11 +6935,6 @@ snapshots:
dependencies:
'@iconify/types': 2.0.0
'@iconify/svelte@5.2.1(svelte@5.49.1)':
dependencies:
'@iconify/types': 2.0.0
svelte: 5.49.1
'@iconify/tools@4.2.0':
dependencies:
'@iconify/types': 2.0.0
@@ -9199,10 +9180,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
iconify-icon@1.0.7:
dependencies:
'@iconify/types': 2.0.0
iconv-lite@0.6.3:
dependencies:
safer-buffer: 2.1.2

File diff suppressed because one or more lines are too long

135
scripts/collect-icons.mjs Normal file
View File

@@ -0,0 +1,135 @@
import { readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from "fs";
import { join, relative } from "path";
const ROOT = join(import.meta.dirname, "..");
const SRC = join(ROOT, "src");
const OUT_DIR = join(SRC, "generated");
const OUT_FILE = join(OUT_DIR, "icon-inventory.json");
const OUT_INCLUDE = join(OUT_DIR, "icon-include.mjs");
/** @type {Record<string, string>} */
const ICON_ALIASES = {
"eos-icons:loading": "material-symbols:progress-activity",
};
const KNOWN_PREFIXES = new Set([
"material-symbols",
"fa6-brands",
"fa6-regular",
"fa6-solid",
"mdi",
"logos",
"devicon",
"hugeicons",
"mingcute",
"skill-icons",
"vscode-icons",
"material-icon-theme",
"game-icons",
"iconoir",
]);
const ICON_PATTERN =
/(?:name|icon)=["'{]([a-z0-9][a-z0-9-]*:[a-z0-9][a-z0-9._-]*)/gi;
const JSON_ICON_PATTERN = /"icon"\s*:\s*"([^"]+)"/g;
const YAML_ICON_PATTERN = /\bicon:\s*["']([^"']+)["']/g;
const TS_ICON_PATTERN = /\bicon\s*:\s*["']([^"']+)["']/g;
const RETURN_ICON_PATTERN =
/return\s+["']([a-z0-9][a-z0-9-]*:[a-z0-9][a-z0-9._-]*)["']/g;
function walk(dir, files = []) {
for (const entry of readdirSync(dir)) {
const full = join(dir, entry);
if (statSync(full).isDirectory()) {
walk(full, files);
} else {
files.push(full);
}
}
return files;
}
function normalizeIcon(name) {
const trimmed = name.trim();
return ICON_ALIASES[trimmed] ?? trimmed;
}
function addIcon(icons, raw) {
const name = normalizeIcon(raw);
if (!name.includes(":")) return;
const [prefix, iconName] = name.split(":");
if (!prefix || !iconName || !KNOWN_PREFIXES.has(prefix)) return;
if (!icons[prefix]) icons[prefix] = new Set();
icons[prefix].add(iconName);
}
function scanFile(path, icons) {
const content = readFileSync(path, "utf8");
const rel = relative(ROOT, path).replace(/\\/g, "/");
if (rel.endsWith(".json")) {
for (const match of content.matchAll(JSON_ICON_PATTERN)) {
addIcon(icons, match[1]);
}
return;
}
if (rel.endsWith(".yaml") || rel.endsWith(".yml")) {
for (const match of content.matchAll(YAML_ICON_PATTERN)) {
addIcon(icons, match[1]);
}
return;
}
if (!/\.(astro|svelte|ts|tsx|js|mjs)$/.test(rel)) return;
for (const match of content.matchAll(ICON_PATTERN)) {
addIcon(icons, match[1]);
}
for (const match of content.matchAll(TS_ICON_PATTERN)) {
addIcon(icons, match[1]);
}
for (const match of content.matchAll(RETURN_ICON_PATTERN)) {
addIcon(icons, match[1]);
}
}
function main() {
/** @type {Record<string, Set<string>>} */
const icons = {};
const files = walk(SRC);
for (const file of files) {
if (file.includes("generated")) continue;
scanFile(file, icons);
}
const extraConfigFiles = [join(ROOT, "twilight.config.yaml")];
for (const file of extraConfigFiles) {
try {
scanFile(file, icons);
} catch {
/* optional config */
}
}
/** @type {Record<string, string[]>} */
const inventory = {};
for (const [prefix, names] of Object.entries(icons)) {
inventory[prefix] = [...names].sort();
}
mkdirSync(OUT_DIR, { recursive: true });
writeFileSync(OUT_FILE, `${JSON.stringify(inventory, null, 2)}\n`, "utf8");
writeFileSync(
OUT_INCLUDE,
`// AUTO-GENERATED by scripts/collect-icons.mjs — do not edit\nexport default ${JSON.stringify(inventory, null, 4)};\n`,
"utf8",
);
const total = Object.values(inventory).reduce((n, arr) => n + arr.length, 0);
console.log(`Collected ${total} icons in ${Object.keys(inventory).length} sets -> ${relative(ROOT, OUT_FILE)}`);
}
main();

View File

@@ -1,9 +0,0 @@
const { copyFileSync, mkdirSync } = require("fs");
const { dirname, join } = require("path");
const src = join(__dirname, "../node_modules/iconify-icon/dist/iconify-icon.min.js");
const dest = join(__dirname, "../public/assets/iconify/iconify-icon.min.js");
mkdirSync(dirname(dest), { recursive: true });
copyFileSync(src, dest);
console.log(`Copied iconify-icon.min.js to ${dest}`);

View File

@@ -0,0 +1,92 @@
import { readFileSync, writeFileSync, mkdirSync } from "fs";
import { join } from "path";
import { createRequire } from "module";
import { getIconData, iconToSVG } from "@iconify/utils";
const require = createRequire(import.meta.url);
const ROOT = join(import.meta.dirname, "..");
const INVENTORY_FILE = join(ROOT, "src/generated/icon-inventory.json");
const OUT_DIR = join(ROOT, "src/generated");
const OUT_FILE = join(OUT_DIR, "site-icon-svgs.ts");
const PACKAGE_BY_PREFIX = {
"fa6-brands": "@iconify-json/fa6-brands",
"fa6-regular": "@iconify-json/fa6-regular",
"fa6-solid": "@iconify-json/fa6-solid",
mdi: "@iconify-json/mdi",
"material-symbols": "@iconify-json/material-symbols",
logos: "@iconify-json/logos",
devicon: "@iconify-json/devicon",
hugeicons: "@iconify-json/hugeicons",
mingcute: "@iconify-json/mingcute",
"skill-icons": "@iconify-json/skill-icons",
"vscode-icons": "@iconify-json/vscode-icons",
"material-icon-theme": "@iconify-json/material-icon-theme",
"game-icons": "@iconify-json/game-icons",
iconoir: "@iconify-json/iconoir",
};
function loadIconSet(prefix) {
const pkg = PACKAGE_BY_PREFIX[prefix];
if (!pkg) return null;
return require(`${pkg}/icons.json`);
}
function buildSvg(iconName) {
const [prefix, name] = iconName.split(":");
const iconSet = loadIconSet(prefix);
if (!iconSet) {
console.warn(`Unknown icon set: ${prefix} (${iconName})`);
return null;
}
const data = getIconData(iconSet, name);
if (!data) {
console.warn(`Icon not found: ${iconName}`);
return null;
}
const { body, attributes } = iconToSVG(data, { height: "1em" });
const attrs = Object.entries(attributes)
.map(([k, v]) => `${k}="${v}"`)
.join(" ");
return `<svg xmlns="http://www.w3.org/2000/svg" ${attrs}>${body}</svg>`;
}
function main() {
const inventory = JSON.parse(readFileSync(INVENTORY_FILE, "utf8"));
/** @type {Record<string, string>} */
const svgs = {};
for (const [prefix, names] of Object.entries(inventory)) {
for (const name of names) {
const fullName = `${prefix}:${name}`;
const svg = buildSvg(fullName);
if (svg) svgs[fullName] = svg;
}
}
const fallback =
svgs["material-symbols:help-outline"] ??
svgs["material-symbols:info"] ??
Object.values(svgs)[0] ??
"";
const lines = [
"// Generated by scripts/generate-site-icons.mjs — do not edit manually",
"",
`export const siteIconFallback = ${JSON.stringify(fallback)};`,
"",
"export const siteIconSvgs: Record<string, string> = {",
];
for (const [name, svg] of Object.entries(svgs).sort(([a], [b]) => a.localeCompare(b))) {
lines.push(` ${JSON.stringify(name)}: ${JSON.stringify(svg)},`);
}
lines.push("};", "");
mkdirSync(OUT_DIR, { recursive: true });
writeFileSync(OUT_FILE, `${lines.join("\n")}\n`, "utf8");
console.log(`Generated ${Object.keys(svgs).length} SVGs -> ${OUT_FILE}`);
}
main();

View File

@@ -0,0 +1,17 @@
import { readFileSync } from "fs";
import { join } from "path";
const inventoryPath = join(import.meta.dirname, "../src/generated/icon-inventory.json");
export function loadIconInventory() {
return JSON.parse(readFileSync(inventoryPath, "utf8"));
}
export function toAstroIconInclude(inventory) {
/** @type {Record<string, string[]>} */
const include = {};
for (const [prefix, icons] of Object.entries(inventory)) {
include[prefix] = icons;
}
return include;
}

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { siteIconFallback, siteIconSvgs } from "@/generated/site-icon-svgs";
interface Props {
icon: string;
class?: string;
}
let { icon, class: className = "" }: Props = $props();
const svg = $derived(siteIconSvgs[icon] ?? siteIconFallback);
</script>
<span class={`inline-flex items-center justify-center shrink-0 [&>svg]:w-[1em] [&>svg]:h-[1em] ${className}`}>
{@html svg}
</span>

View File

@@ -1,6 +1,5 @@
---
// 可靠的图标组件
// 提供加载状态管理和错误处理
import { Icon } from "astro-icon/components";
export interface Props {
icon: string;
@@ -8,8 +7,6 @@ export interface Props {
style?: string;
size?: "xs" | "sm" | "md" | "lg" | "xl" | "2xl";
color?: string;
fallback?: string; // 备用图标或文本
loading?: "lazy" | "eager";
}
const {
@@ -18,11 +15,8 @@ const {
style = "",
size = "md",
color,
fallback = "●",
loading = "lazy",
} = Astro.props;
// 尺寸映射
const sizeClasses = {
xs: "text-xs",
sm: "text-sm",
@@ -35,147 +29,7 @@ const sizeClasses = {
const sizeClass = sizeClasses[size] || sizeClasses.md;
const colorStyle = color ? `color: ${color};` : "";
const combinedStyle = `${colorStyle}${style}`;
const combinedClass = `${sizeClass} ${className}`.trim();
// 生成唯一ID
const iconId = `icon-${Math.random().toString(36).substr(2, 9)}`;
const combinedClass = `inline-flex items-center justify-center shrink-0 ${sizeClass} ${className}`.trim();
---
<span
class={`inline-flex items-center justify-center ${combinedClass}`}
style={combinedStyle}
data-icon-container={iconId}
>
<!-- 加载状态指示器 -->
<span
class="icon-loading animate-pulse opacity-50"
data-loading-indicator
>
{fallback}
</span>
<!-- 实际图标 -->
<iconify-icon
icon={icon}
class="icon-content opacity-0 transition-opacity duration-200"
data-icon-element
loading={loading}
></iconify-icon>
</span>
<script define:vars={{ iconId, icon }}>
// 图标加载和显示逻辑
(function() {
const container = document.querySelector(`[data-icon-container="${iconId}"]`);
if (!container) return;
const loadingIndicator = container.querySelector('[data-loading-indicator]');
const iconElement = container.querySelector('[data-icon-element]');
if (!loadingIndicator || !iconElement) return;
// 检查图标是否已经加载
function checkIconLoaded() {
// 检查iconify-icon元素是否已经渲染
const hasContent = iconElement.shadowRoot &&
iconElement.shadowRoot.children.length > 0;
if (hasContent) {
showIcon();
return true;
}
return false;
}
// 显示图标,隐藏加载指示器
function showIcon() {
loadingIndicator.style.display = 'none';
iconElement.classList.remove('opacity-0');
iconElement.classList.add('opacity-100');
}
// 显示加载指示器,隐藏图标
function showLoading() {
loadingIndicator.style.display = 'inline-flex';
iconElement.classList.remove('opacity-100');
iconElement.classList.add('opacity-0');
}
// 初始状态
showLoading();
// 监听图标加载事件
iconElement.addEventListener('load', () => {
showIcon();
});
// 监听图标加载错误
iconElement.addEventListener('error', () => {
// 保持显示fallback
console.warn(`Failed to load icon: ${icon}`);
});
// 使用MutationObserver监听shadow DOM变化
if (window.MutationObserver) {
const observer = new MutationObserver(() => {
if (checkIconLoaded()) {
observer.disconnect();
}
});
// 监听iconify-icon元素的变化
observer.observe(iconElement, {
childList: true,
subtree: true,
attributes: true
});
// 设置超时,避免无限等待
setTimeout(() => {
observer.disconnect();
if (!checkIconLoaded()) {
console.warn(`Icon load timeout: ${icon}`);
}
}, 5000);
}
// 立即检查一次(可能已经加载完成)
setTimeout(() => {
checkIconLoaded();
}, 100);
})();
</script>
<style>
.icon-loading {
min-width: 1em;
min-height: 1em;
display: inline-flex;
align-items: center;
justify-content: center;
}
.icon-content {
display: inline-flex;
align-items: center;
justify-content: center;
}
[data-icon-container] {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1em;
min-height: 1em;
}
[data-icon-container] .icon-loading,
[data-icon-container] .icon-content {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
<Icon name={icon} class={combinedClass} style={combinedStyle} />

View File

@@ -1,248 +0,0 @@
---
import { url } from "@utils/url";
export interface Props {
preloadIcons?: string[];
}
const { preloadIcons = [] } = Astro.props;
const iconifyScriptUrl = url("/assets/iconify/iconify-icon.min.js");
---
<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;
this.isLoading = false;
this.loadPromise = null;
this.observers = new Set();
this.preloadQueue = new Set();
}
async load(options = {}) {
const {
timeout: loadTimeout = LOAD_TIMEOUT_MS,
retryCount: maxRetries = RETRY_COUNT,
} = options;
if (this.isLoaded) {
return Promise.resolve();
}
if (this.isLoading && this.loadPromise) {
return this.loadPromise;
}
this.isLoading = true;
this.loadPromise = this.loadWithRetry(loadTimeout, maxRetries);
try {
await this.loadPromise;
this.isLoaded = true;
this.notifyObservers();
await this.processPreloadQueue();
} catch (error) {
console.error("Failed to load Iconify:", error);
throw error;
} finally {
this.isLoading = false;
}
}
async loadWithRetry(timeoutMs, maxRetries) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await this.loadScript(timeoutMs);
return;
} catch (error) {
console.warn(`Iconify load attempt ${attempt} failed:`, error);
if (attempt === maxRetries) {
throw new Error(
`Failed to load Iconify after ${maxRetries} attempts`,
);
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
}
}
loadScript(timeoutMs) {
return new Promise((resolve, reject) => {
if (this.isIconifyReady()) {
registerWebComponentIconifyCollections();
resolve();
return;
}
const existingScript = document.querySelector(
'script[src*="iconify-icon"]',
);
if (existingScript) {
this.waitForIconifyReady()
.then(() => {
registerWebComponentIconifyCollections();
resolve();
})
.catch(reject);
return;
}
const script = document.createElement("script");
script.src = iconifyScriptUrl;
script.async = true;
const timeoutId = setTimeout(() => {
script.remove();
reject(new Error("Script load timeout"));
}, timeoutMs);
script.onload = () => {
clearTimeout(timeoutId);
this.waitForIconifyReady()
.then(() => {
registerWebComponentIconifyCollections();
resolve();
})
.catch(reject);
};
script.onerror = () => {
clearTimeout(timeoutId);
script.remove();
reject(new Error("Script load error"));
};
document.head.appendChild(script);
});
}
waitForIconifyReady(maxWait = 5000) {
return new Promise((resolve, reject) => {
const startTime = Date.now();
const checkReady = () => {
if (this.isIconifyReady()) {
resolve();
return;
}
if (Date.now() - startTime > maxWait) {
reject(new Error("Iconify initialization timeout"));
return;
}
setTimeout(checkReady, 50);
};
checkReady();
});
}
isIconifyReady() {
return (
typeof window !== "undefined" &&
"customElements" in window &&
customElements.get("iconify-icon") !== undefined
);
}
onLoad(callback) {
if (this.isLoaded) {
callback();
} else {
this.observers.add(callback);
}
}
notifyObservers() {
this.observers.forEach((callback) => {
try {
callback();
} catch (error) {
console.error("Error in icon load observer:", error);
}
});
this.observers.clear();
}
addToPreloadQueue(icons) {
if (Array.isArray(icons)) {
icons.forEach((icon) => this.preloadQueue.add(icon));
} else {
this.preloadQueue.add(icons);
}
if (this.isLoaded) {
this.processPreloadQueue();
}
}
async processPreloadQueue() {
this.preloadQueue.clear();
}
}
window.__iconifyLoader = new IconifyLoader();
window.__iconifyLoader.load().catch((error) => {
console.error("Failed to initialize Iconify:", error);
});
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);
document.addEventListener("visibilitychange", () => {
if (!document.hidden && !window.__iconifyLoader.isLoaded) {
window.__iconifyLoader.load().catch(console.error);
}
});
})();
</script>
<noscript>
<style>
iconify-icon {
display: none;
}
.icon-fallback {
display: inline-block;
}
</style>
</noscript>

View File

@@ -218,18 +218,3 @@ const getExperienceText = (experience: Props["skill"]["experience"]) => {
</div>
</div>
</div>
<script>
// 监听图标加载完成事件
document.addEventListener('DOMContentLoaded', () => {
const skillCard = document.currentScript?.parentElement;
if (skillCard) {
skillCard.classList.add('skill-card');
// 监听图标准备就绪事件
skillCard.addEventListener('iconify-ready', () => {
// 图标加载完成,可以执行额外的初始化逻辑
skillCard.classList.add('icons-loaded');
});
}
});
</script>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import { onDestroy, onMount } from "svelte";
import { slide } from "svelte/transition";
import Icon from "@iconify/svelte";
import SiteIcon from "@components/common/SiteIcon.svelte";
import type { MusicPlayerTrack } from "@/types/config";
import { musicPlayerConfig } from "@/config";
@@ -557,10 +557,10 @@ onDestroy(() => {
{#if showError}
<div class="music-player-error fixed bottom-20 right-4 z-60 max-w-sm onload-animation-up">
<div class="bg-red-500 text-white px-4 py-3 rounded-lg shadow-lg flex items-center gap-3 animate-slide-up">
<Icon icon="material-symbols:error" class="text-xl shrink-0" />
<SiteIcon icon="material-symbols:error" class="text-xl shrink-0" />
<span class="text-sm flex-1">{errorMessage}</span>
<button onclick={hideError} class="text-white/80 hover:text-white transition-colors">
<Icon icon="material-symbols:close" class="text-lg" />
<SiteIcon icon="material-symbols:close" class="text-lg" />
</button>
</div>
</div>
@@ -575,7 +575,7 @@ onDestroy(() => {
<div class="playlist-header flex items-center justify-between p-4 border-b border-(--line-divider)">
<h3 class="text-lg font-semibold text-90">{i18n(Key.playlist)}</h3>
<button class="btn-plain w-8 h-8 rounded-lg" onclick={togglePlaylist}>
<Icon icon="material-symbols:close" class="text-lg" />
<SiteIcon icon="material-symbols:close" class="text-lg" />
</button>
</div>
<div class="playlist-content overflow-y-auto max-h-80">
@@ -595,9 +595,9 @@ onDestroy(() => {
aria-label="播放 {song.title} - {song.artist}">
<div class="w-6 h-6 flex items-center justify-center">
{#if index === currentIndex && isPlaying}
<Icon icon="material-symbols:graphic-eq" class="text-(--primary) animate-pulse" />
<SiteIcon icon="material-symbols:graphic-eq" class="text-(--primary) animate-pulse" />
{:else if index === currentIndex}
<Icon icon="material-symbols:pause" class="text-(--primary)" />
<SiteIcon icon="material-symbols:pause" class="text-(--primary)" />
{:else}
<span class="text-sm text-(--content-meta)">{index + 1}</span>
{/if}
@@ -636,7 +636,7 @@ onDestroy(() => {
tabindex="0"
aria-label={i18n(Key.musicExpand)}>
{#if isLoading}
<Icon icon="eos-icons:loading" class="text-white text-lg" />
<SiteIcon icon="material-symbols:progress-activity" class="text-white text-lg" />
{:else if isPlaying}
<div class="flex space-x-0.5">
<div class="w-0.5 h-3 bg-white rounded-full animate-pulse"></div>
@@ -644,7 +644,7 @@ onDestroy(() => {
<div class="w-0.5 h-2 bg-white rounded-full animate-pulse" style="animation-delay: 300ms;"></div>
</div>
{:else}
<Icon icon="material-symbols:music-note" class="text-white text-lg" />
<SiteIcon icon="material-symbols:music-note" class="text-white text-lg" />
{/if}
</div>
<!-- 展开状态的完整播放器(封面圆形) -->
@@ -671,13 +671,13 @@ onDestroy(() => {
<button class="btn-plain w-8 h-8 rounded-lg flex items-center justify-center"
onclick={toggleMode}
title={mode === "meting" ? i18n(Key.musicSwitchToLocal) : i18n(Key.musicSwitchToMeting)}>
<Icon icon={mode === "meting" ? "material-symbols:cloud" : "material-symbols:folder"} class="text-lg" />
<SiteIcon icon={mode === "meting" ? "material-symbols:cloud" : "material-symbols:folder"} class="text-lg" />
</button>
<button class="btn-plain w-8 h-8 rounded-lg flex items-center justify-center"
class:text-(--primary)={showPlaylist}
onclick={togglePlaylist}
title={i18n(Key.playlist)}>
<Icon icon="material-symbols:queue-music" class="text-lg" />
<SiteIcon icon="material-symbols:queue-music" class="text-lg" />
</button>
</div>
</div>
@@ -717,27 +717,27 @@ onDestroy(() => {
class:btn-plain={!isShuffled}
onclick={toggleShuffle}
disabled={playlist.length <= 1}>
<Icon icon="material-symbols:shuffle" class="text-lg" />
<SiteIcon icon="material-symbols:shuffle" class="text-lg" />
</button>
<button class="btn-plain w-10 h-10 rounded-lg" onclick={previousSong}
disabled={playlist.length <= 1}>
<Icon icon="material-symbols:skip-previous" class="text-xl" />
<SiteIcon icon="material-symbols:skip-previous" class="text-xl" />
</button>
<button class="btn-regular w-12 h-12 rounded-full"
class:opacity-50={isLoading}
disabled={isLoading}
onclick={togglePlay}>
{#if isLoading}
<Icon icon="eos-icons:loading" class="text-xl" />
<SiteIcon icon="material-symbols:progress-activity" class="text-xl" />
{:else if isPlaying}
<Icon icon="material-symbols:pause" class="text-xl" />
<SiteIcon icon="material-symbols:pause" class="text-xl" />
{:else}
<Icon icon="material-symbols:play-arrow" class="text-xl" />
<SiteIcon icon="material-symbols:play-arrow" class="text-xl" />
{/if}
</button>
<button class="btn-plain w-10 h-10 rounded-lg" onclick={nextSong}
disabled={playlist.length <= 1}>
<Icon icon="material-symbols:skip-next" class="text-xl" />
<SiteIcon icon="material-symbols:skip-next" class="text-xl" />
</button>
<!-- 循环按钮高亮 -->
<button class="w-10 h-10 rounded-lg"
@@ -745,22 +745,22 @@ onDestroy(() => {
class:btn-plain={isRepeating === 0}
onclick={toggleRepeat}>
{#if isRepeating === 1}
<Icon icon="material-symbols:repeat-one" class="text-lg" />
<SiteIcon icon="material-symbols:repeat-one" class="text-lg" />
{:else if isRepeating === 2}
<Icon icon="material-symbols:repeat" class="text-lg" />
<SiteIcon icon="material-symbols:repeat" class="text-lg" />
{:else}
<Icon icon="material-symbols:repeat" class="text-lg opacity-50" />
<SiteIcon icon="material-symbols:repeat" class="text-lg opacity-50" />
{/if}
</button>
</div>
<div class="bottom-controls flex items-center gap-2">
<button class="btn-plain w-8 h-8 rounded-lg" onclick={toggleMute}>
{#if isMuted || volume === 0}
<Icon icon="material-symbols:volume-off" class="text-lg" />
<SiteIcon icon="material-symbols:volume-off" class="text-lg" />
{:else if volume < 0.5}
<Icon icon="material-symbols:volume-down" class="text-lg" />
<SiteIcon icon="material-symbols:volume-down" class="text-lg" />
{:else}
<Icon icon="material-symbols:volume-up" class="text-lg" />
<SiteIcon icon="material-symbols:volume-up" class="text-lg" />
{/if}
</button>
<div class="flex-1 h-2 bg-(--btn-regular-bg) rounded-full cursor-pointer"
@@ -787,7 +787,7 @@ onDestroy(() => {
<button class="btn-plain w-8 h-8 rounded-lg flex items-center justify-center"
onclick={toggleCollapse}
title={i18n(Key.musicCollapse)}>
<Icon icon="material-symbols:expand-more" class="text-lg" />
<SiteIcon icon="material-symbols:expand-more" class="text-lg" />
</button>
</div>
</div>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import SiteIcon from "@components/common/SiteIcon.svelte";
import { onMount } from "svelte";
import { BREAKPOINT_LG } from "@constants/breakpoints";
@@ -58,7 +58,7 @@ $effect(() => {
onclick={() => { if (window.innerWidth < BREAKPOINT_LG) { openPanel(); } else { togglePanel(); } }}
onmouseenter={openPanel}
>
<Icon icon="material-symbols:palette-outline" class="text-[1.25rem]"></Icon>
<SiteIcon icon="material-symbols:palette-outline" class="text-[1.25rem]"/>
</button>
<div id="display-setting-wrapper" class="fixed top-14.5 pt-5 right-4 w-[calc(100vw-2rem)] max-w-80 md:absolute md:top-11 md:right-0 md:w-80 md:pt-5 transition-all z-50" class:float-panel-closed={!isOpen}>
<div id="display-setting" class="card-base float-panel px-4 py-4 w-full">
@@ -71,7 +71,7 @@ $effect(() => {
<button aria-label="Reset to Default" class="btn-regular w-7 h-7 rounded-md active:scale-90"
class:opacity-0={hue === defaultHue} class:pointer-events-none={hue === defaultHue} onclick={resetHue}>
<div class="text-(--btn-content)">
<Icon icon="fa6-solid:arrow-rotate-left" class="text-[0.875rem]"></Icon>
<SiteIcon icon="fa6-solid:arrow-rotate-left" class="text-[0.875rem]"/>
</div>
</button>
</div>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import SiteIcon from "@components/common/SiteIcon.svelte";
import { onMount } from "svelte";
import { BREAKPOINT_LG } from "@constants/breakpoints";
@@ -66,13 +66,13 @@ onMount(() => {
<div class="relative z-50" role="menu" tabindex="-1" onmouseleave={closePanel}>
<button aria-label="Light/Dark/System Mode" role="menuitem" class="relative btn-plain scale-animation rounded-lg h-11 w-11 active:scale-90" id="scheme-switch" onmouseenter={openPanel} onclick={() => { if (window.innerWidth < BREAKPOINT_LG) { openPanel(); } else { toggleScheme(); } }}>
<div class="absolute" class:opacity-0={mode !== LIGHT_MODE}>
<Icon icon="material-symbols:wb-sunny-outline-rounded" class="text-[1.25rem]"></Icon>
<SiteIcon icon="material-symbols:wb-sunny-outline-rounded" class="text-[1.25rem]"/>
</div>
<div class="absolute" class:opacity-0={mode !== DARK_MODE}>
<Icon icon="material-symbols:dark-mode-outline-rounded" class="text-[1.25rem]"></Icon>
<SiteIcon icon="material-symbols:dark-mode-outline-rounded" class="text-[1.25rem]"/>
</div>
<div class="absolute" class:opacity-0={mode !== SYSTEM_MODE}>
<Icon icon="material-symbols:radio-button-partial-outline" class="text-[1.25rem]"></Icon>
<SiteIcon icon="material-symbols:radio-button-partial-outline" class="text-[1.25rem]"/>
</div>
</button>
<div id="light-dark-panel" class="absolute transition top-11 -right-2 pt-5" class:float-panel-closed={!isOpen}>
@@ -82,7 +82,7 @@ onMount(() => {
isLast={false}
onclick={() => switchScheme(LIGHT_MODE)}
>
<Icon icon="material-symbols:wb-sunny-outline-rounded" class="text-[1.25rem] mr-3"></Icon>
<SiteIcon icon="material-symbols:wb-sunny-outline-rounded" class="text-[1.25rem] mr-3"/>
{i18n(I18nKey.lightMode)}
</DropdownItem>
<DropdownItem
@@ -90,7 +90,7 @@ onMount(() => {
isLast={false}
onclick={() => switchScheme(DARK_MODE)}
>
<Icon icon="material-symbols:dark-mode-outline-rounded" class="text-[1.25rem] mr-3"></Icon>
<SiteIcon icon="material-symbols:dark-mode-outline-rounded" class="text-[1.25rem] mr-3"/>
{i18n(I18nKey.darkMode)}
</DropdownItem>
<DropdownItem
@@ -98,7 +98,7 @@ onMount(() => {
isLast={true}
onclick={() => switchScheme(SYSTEM_MODE)}
>
<Icon icon="material-symbols:radio-button-partial-outline" class="text-[1.25rem] mr-3"></Icon>
<SiteIcon icon="material-symbols:radio-button-partial-outline" class="text-[1.25rem] mr-3"/>
{i18n(I18nKey.systemMode)}
</DropdownItem>
</DropdownPanel>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import SiteIcon from "@components/common/SiteIcon.svelte";
import { onMount } from "svelte";
import type { NavbarLink } from "@/types/config";
@@ -39,7 +39,7 @@ onMount(() => {
id="nav-menu-switch"
onclick={togglePanel}
>
<Icon icon="material-symbols:menu-rounded" class="text-[1.25rem]"></Icon>
<SiteIcon icon="material-symbols:menu-rounded" class="text-[1.25rem]"/>
</button>
<div id="nav-menu-panel"
class="float-panel fixed transition-all right-4 px-2 py-2 max-h-[80vh] overflow-y-auto"
@@ -53,14 +53,14 @@ onMount(() => {
>
<div class="flex items-center transition text-black/75 dark:text-white/75 font-bold group-hover:text-(--primary) group-active:text-(--primary)">
{#if link.icon}
<Icon icon={link.icon} class="text-[1.1rem] mr-2" />
<SiteIcon icon={link.icon} class="text-[1.1rem] mr-2" />
{/if}
{link.name}
</div>
{#if !link.external}
<Icon icon="material-symbols:chevron-right-rounded" class="transition text-[1.25rem] text-(--primary)" />
<SiteIcon icon="material-symbols:chevron-right-rounded" class="transition text-[1.25rem] text-(--primary)" />
{:else}
<Icon icon="fa6-solid:arrow-up-right-from-square" class="transition text-[0.75rem] text-black/25 dark:text-white/25 -translate-x-1" />
<SiteIcon icon="fa6-solid:arrow-up-right-from-square" class="transition text-[0.75rem] text-black/25 dark:text-white/25 -translate-x-1" />
{/if}
</a>
</div>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { onDestroy, onMount } from "svelte";
import Icon from "@iconify/svelte";
import SiteIcon from "@components/common/SiteIcon.svelte";
import { BREAKPOINT_LG } from "@constants/breakpoints";
import { getTranslateLanguageFromConfig, getSiteLanguage, setStoredLanguage, getDefaultLanguage } from "@/utils/language";
@@ -99,7 +99,7 @@ onDestroy(() => {
onclick={() => { if (window.innerWidth < BREAKPOINT_LG) { openPanel(); } else { togglePanel(); } }}
onmouseenter={openPanel}
>
<Icon icon="material-symbols:translate" class="text-[1.25rem] transition" />
<SiteIcon icon="material-symbols:translate" class="text-[1.25rem] transition" />
</button>
<!-- 翻译面板 -->
<div id="translate-panel-wrapper" class="fixed top-14.5 pt-5 right-4 w-[calc(100vw-2rem)] max-w-64 md:absolute md:top-11 md:right-0 md:w-64 md:pt-5 transition-all z-50" class:float-panel-closed={!isOpen}>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import Icon from "@iconify/svelte";
import SiteIcon from "@components/common/SiteIcon.svelte";
import { onMount } from "svelte";
import { BREAKPOINT_LG } from "@/constants/breakpoints";
@@ -66,13 +66,13 @@ onMount(() => {
<div class="relative z-50" role="menu" tabindex="-1" onmouseleave={closePanel}>
<button aria-label="Wallpaper Mode" role="menuitem" class="relative btn-plain scale-animation rounded-lg h-11 w-11 active:scale-90" id="wallpaper-mode-switch" onmouseenter={openPanel} onclick={() => { if (window.innerWidth < BREAKPOINT_LG) { openPanel(); } else { toggleWallpaperMode(); } }}>
<div class="absolute" class:opacity-0={mode !== WALLPAPER_BANNER}>
<Icon icon="material-symbols:image-outline" class="text-[1.25rem]"></Icon>
<SiteIcon icon="material-symbols:image-outline" class="text-[1.25rem]"/>
</div>
<div class="absolute" class:opacity-0={mode !== WALLPAPER_FULLSCREEN}>
<Icon icon="material-symbols:wallpaper" class="text-[1.25rem]"></Icon>
<SiteIcon icon="material-symbols:wallpaper" class="text-[1.25rem]"/>
</div>
<div class="absolute" class:opacity-0={mode !== WALLPAPER_NONE}>
<Icon icon="material-symbols:hide-image-outline" class="text-[1.25rem]"></Icon>
<SiteIcon icon="material-symbols:hide-image-outline" class="text-[1.25rem]"/>
</div>
</button>
<div id="wallpaper-mode-panel" class="absolute transition top-11 -right-2 pt-5" class:float-panel-closed={!isOpen}>
@@ -82,7 +82,7 @@ onMount(() => {
isLast={false}
onclick={() => switchWallpaperMode(WALLPAPER_BANNER)}
>
<Icon icon="material-symbols:image-outline" class="text-[1.25rem] mr-3"></Icon>
<SiteIcon icon="material-symbols:image-outline" class="text-[1.25rem] mr-3"/>
{i18n(I18nKey.wallpaperBanner)}
</DropdownItem>
<DropdownItem
@@ -90,7 +90,7 @@ onMount(() => {
isLast={false}
onclick={() => switchWallpaperMode(WALLPAPER_FULLSCREEN)}
>
<Icon icon="material-symbols:wallpaper" class="text-[1.25rem] mr-3"></Icon>
<SiteIcon icon="material-symbols:wallpaper" class="text-[1.25rem] mr-3"/>
{i18n(I18nKey.wallpaperFullscreen)}
</DropdownItem>
<DropdownItem
@@ -98,7 +98,7 @@ onMount(() => {
isLast={true}
onclick={() => switchWallpaperMode(WALLPAPER_NONE)}
>
<Icon icon="material-symbols:hide-image-outline" class="text-[1.25rem] mr-3"></Icon>
<SiteIcon icon="material-symbols:hide-image-outline" class="text-[1.25rem] mr-3"/>
{i18n(I18nKey.wallpaperNone)}
</DropdownItem>
</DropdownPanel>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { onMount } from 'svelte';
import Icon from "@iconify/svelte";
import SiteIcon from "@components/common/SiteIcon.svelte";
import dayjs from 'dayjs';
import { BREAKPOINT_LG } from "@constants/breakpoints";
@@ -444,7 +444,7 @@
<button class="time-scale-select flex items-center gap-1">
{labels[timeScale]}
<span class="dropdown-icon flex items-center">
<Icon icon="material-symbols:keyboard-arrow-down-rounded" />
<SiteIcon icon="material-symbols:keyboard-arrow-down-rounded" />
</span>
</button>
<div class="dropdown-menu-custom">

View File

@@ -0,0 +1,137 @@
// AUTO-GENERATED by scripts/collect-icons.mjs — do not edit
export default {
"material-symbols": [
"archive",
"article",
"book",
"book-2-outline-rounded",
"calendar-today-outline-rounded",
"chevron-left-rounded",
"chevron-right-rounded",
"close",
"code",
"copyright-outline-rounded",
"dark-mode-outline-rounded",
"edit-calendar-outline-rounded",
"emoji-events",
"error",
"error-outline",
"event",
"expand-more",
"format-list-bulleted-rounded",
"graphic-eq",
"group",
"help-outline",
"hide-image-outline",
"history-rounded",
"home",
"home-rounded",
"image-outline",
"info",
"keyboard-arrow-down-rounded",
"keyboard-arrow-up-rounded",
"link",
"mail-outline",
"menu-rounded",
"more-horiz",
"movie",
"music-note",
"notes-rounded",
"palette-outline",
"pause",
"photo-library",
"play-arrow",
"progress-activity",
"psychology",
"queue-music",
"radio-button-partial-outline",
"repeat",
"repeat-one",
"rss-feed",
"schedule-outline-rounded",
"school",
"search-off",
"security",
"sentiment-sad",
"shuffle",
"skip-next",
"skip-previous",
"star",
"tag-rounded",
"timeline",
"translate",
"visibility-outline-rounded",
"volume-down",
"volume-off",
"volume-up",
"wallpaper",
"wb-sunny-outline-rounded",
"work"
],
"fa6-solid": [
"arrow-rotate-left",
"arrow-up-right-from-square",
"eye",
"xmark"
],
"fa6-brands": [
"creative-commons",
"git-alt",
"telegram"
],
"mdi": [
"car-outline",
"certificate-outline",
"pin"
],
"fa6-regular": [
"address-card"
],
"logos": [
"angular-icon",
"c-plusplus",
"css-3",
"docker-icon",
"git-icon",
"html-5",
"javascript",
"linux-tux",
"nestjs",
"nextjs-icon",
"openai-icon",
"python",
"react",
"typescript-icon",
"vue"
],
"skill-icons": [
"neovim-dark"
],
"vscode-icons": [
"file-type-cursorrules"
],
"devicon": [
"java",
"postgresql"
],
"material-icon-theme": [
"kotlin"
],
"hugeicons": [
"mentoring",
"office",
"test-tube"
],
"mingcute": [
"bridge-fill",
"robot-line",
"volleyball-fill",
"vue-fill"
],
"iconoir": [
"agile"
],
"game-icons": [
"podium-winner"
]
};

View File

@@ -0,0 +1,136 @@
{
"material-symbols": [
"archive",
"article",
"book",
"book-2-outline-rounded",
"calendar-today-outline-rounded",
"chevron-left-rounded",
"chevron-right-rounded",
"close",
"code",
"copyright-outline-rounded",
"dark-mode-outline-rounded",
"edit-calendar-outline-rounded",
"emoji-events",
"error",
"error-outline",
"event",
"expand-more",
"format-list-bulleted-rounded",
"graphic-eq",
"group",
"help-outline",
"hide-image-outline",
"history-rounded",
"home",
"home-rounded",
"image-outline",
"info",
"keyboard-arrow-down-rounded",
"keyboard-arrow-up-rounded",
"link",
"mail-outline",
"menu-rounded",
"more-horiz",
"movie",
"music-note",
"notes-rounded",
"palette-outline",
"pause",
"photo-library",
"play-arrow",
"progress-activity",
"psychology",
"queue-music",
"radio-button-partial-outline",
"repeat",
"repeat-one",
"rss-feed",
"schedule-outline-rounded",
"school",
"search-off",
"security",
"sentiment-sad",
"shuffle",
"skip-next",
"skip-previous",
"star",
"tag-rounded",
"timeline",
"translate",
"visibility-outline-rounded",
"volume-down",
"volume-off",
"volume-up",
"wallpaper",
"wb-sunny-outline-rounded",
"work"
],
"fa6-solid": [
"arrow-rotate-left",
"arrow-up-right-from-square",
"eye",
"xmark"
],
"fa6-brands": [
"creative-commons",
"git-alt",
"telegram"
],
"mdi": [
"car-outline",
"certificate-outline",
"pin"
],
"fa6-regular": [
"address-card"
],
"logos": [
"angular-icon",
"c-plusplus",
"css-3",
"docker-icon",
"git-icon",
"html-5",
"javascript",
"linux-tux",
"nestjs",
"nextjs-icon",
"openai-icon",
"python",
"react",
"typescript-icon",
"vue"
],
"skill-icons": [
"neovim-dark"
],
"vscode-icons": [
"file-type-cursorrules"
],
"devicon": [
"java",
"postgresql"
],
"material-icon-theme": [
"kotlin"
],
"hugeicons": [
"mentoring",
"office",
"test-tube"
],
"mingcute": [
"bridge-fill",
"robot-line",
"volleyball-fill",
"vue-fill"
],
"iconoir": [
"agile"
],
"game-icons": [
"podium-winner"
]
}

File diff suppressed because one or more lines are too long

5
src/global.d.ts vendored
View File

@@ -6,14 +6,9 @@ declare global {
}
interface Window {
// Define swup type directly since @swup/astro doesn't export AstroIntegration
swup: any;
semifullScrollHandler: (() => void) | null;
closeAnnouncement: () => void;
iconifyLoaded: boolean;
__iconifyLoader: {
load: () => Promise<void>;
};
translate?: {
service: {
use: (service: string) => void;

View File

@@ -8,7 +8,6 @@ import {
MAIN_PANEL_OVERLAPS_BANNER_HEIGHT,
} from "@constants/constants";
import { widgetManager } from "@utils/widget";
import IconifyLoader from "@components/common/iconifyLoader.astro";
import Navbar from "@components/navbar.astro";
import FullscreenWallpaper from "@components/fullscreenWallpaper.astro";
import Banner from "@components/banner.astro";
@@ -88,9 +87,6 @@ const {
setOGTypeArticle={setOGTypeArticle}
postSlug={postSlug}
>
<!-- 全局图标加载器 -->
<IconifyLoader />
<!-- Navbar -->
<slot slot="head" name="head" />
<div

View File

@@ -13,7 +13,6 @@ import {
} from "@utils/skills";
import { i18n } from "@i18n/translation";
import I18nKey from "@i18n/i18nKey";
import IconifyLoader from "@components/common/iconifyLoader.astro";
import SkillCard from "@components/data/skillCard.astro";
import GridLayout from "@layouts/grid.astro";
import BackwardButton from "@components/backwardButton.astro";
@@ -76,13 +75,9 @@ const getCategoryText = (category: string) => {
}
};
// 收集所有技能图标用于预加载
const allIcons = skillsData.map((skill) => skill.icon).filter(Boolean);
---
<GridLayout title={title} description={subtitle}>
<!-- 图标加载器,预加载所有技能图标 -->
<IconifyLoader preloadIcons={allIcons} />
<div class="flex w-full rounded-(--radius-large) overflow-hidden relative min-h-32">
<div class="card-base z-10 px-9 py-6 relative w-full">
<BackwardButton currentPath={Astro.url.pathname} />
@@ -216,34 +211,3 @@ const allIcons = skillsData.map((skill) => skill.icon).filter(Boolean);
</div>
</div>
</GridLayout>
<script>
// 确保图标库已加载,并处理页面导航
document.addEventListener('DOMContentLoaded', () => {
// 如果图标加载器存在,确保图标已加载
if (window.__iconifyLoader) {
window.__iconifyLoader.load().then(() => {
// 图标加载完成后,触发技能卡片的重新渲染
const skillCards = document.querySelectorAll('.skill-card');
skillCards.forEach(card => {
card.dispatchEvent(new CustomEvent('iconify-ready'));
});
}).catch(error => {
console.error('Failed to load icons on skills page:', error);
});
}
});
// 处理页面导航时的图标重新加载
if (typeof window !== 'undefined') {
// 监听页面显示事件(包括前进/后退导航)
window.addEventListener('pageshow', (event) => {
if (event.persisted && window.__iconifyLoader) {
// 页面从缓存恢复,重新检查图标状态
setTimeout(() => {
window.__iconifyLoader.load().catch(console.error);
}, 100);
}
});
}
</script>

View File

@@ -13,7 +13,6 @@ import {
} from "@utils/timeline";
import { i18n } from "@i18n/translation";
import I18nKey from "@i18n/i18nKey";
import IconifyLoader from "@components/common/iconifyLoader.astro";
import TimelineItem from "@components/data/timelineItem.astro";
import GridLayout from "@layouts/grid.astro";
import BackwardButton from "@components/backwardButton.astro";
@@ -22,11 +21,6 @@ import BackwardButton from "@components/backwardButton.astro";
const title = LinkPresets[LinkPreset.Timeline].name;
const subtitle = LinkPresets[LinkPreset.Timeline].description;
// 收集所有时间线图标用于预加载
const allIcons = timelineData
.map((item) => item.icon || getTypeIcon(item.type))
.filter(Boolean);
// 获取时间线统计信息
const stats = getTimelineStats();
const currentItems = getCurrentItems();
@@ -53,8 +47,6 @@ const getTypeIcon = (type: string) => {
---
<GridLayout title={title} description={subtitle}>
<!-- 图标加载器,预加载所有时间线图标 -->
<IconifyLoader preloadIcons={allIcons} />
<div class="flex w-full rounded-(--radius-large) overflow-hidden relative min-h-32">
<div class="card-base z-10 px-9 py-6 relative w-full">
<BackwardButton currentPath={Astro.url.pathname} />
@@ -186,34 +178,3 @@ const getTypeIcon = (type: string) => {
</div>
</div>
</GridLayout>
<script>
// 确保图标库已加载,并处理页面导航
document.addEventListener('DOMContentLoaded', () => {
// 如果图标加载器存在,确保图标已加载
if (window.__iconifyLoader) {
window.__iconifyLoader.load().then(() => {
// 图标加载完成后,触发时间线项目的重新渲染
const timelineItems = document.querySelectorAll('.timeline-item');
timelineItems.forEach(item => {
item.dispatchEvent(new CustomEvent('iconify-ready'));
});
}).catch(error => {
console.error('Failed to load icons on timeline page:', error);
});
}
});
// 处理页面导航时的图标重新加载
if (typeof window !== 'undefined') {
// 监听页面显示事件(包括前进/后退导航)
window.addEventListener('pageshow', (event) => {
if (event.persisted && window.__iconifyLoader) {
// 页面从缓存恢复,重新检查图标状态
setTimeout(() => {
window.__iconifyLoader.load().catch(console.error);
}, 100);
}
});
}
</script>

View File

@@ -1,78 +0,0 @@
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();
}