viconic-react-icons 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1985 -0
- package/dist/index.mjs +1951 -0
- package/package.json +20 -0
- package/src/copyicons-smart-loader.js +2132 -0
- package/src/index.jsx +25 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1985 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// src/index.jsx
|
|
30
|
+
var index_exports = {};
|
|
31
|
+
__export(index_exports, {
|
|
32
|
+
ViconicIcon: () => ViconicIcon,
|
|
33
|
+
default: () => index_default
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
var import_react = __toESM(require("react"));
|
|
37
|
+
|
|
38
|
+
// src/copyicons-smart-loader.js
|
|
39
|
+
(function() {
|
|
40
|
+
"use strict";
|
|
41
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i;
|
|
42
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
if (typeof document !== "undefined") {
|
|
46
|
+
const style = document.createElement("style");
|
|
47
|
+
style.textContent = ":where(viconic-icon) { display: inline-flex; align-items: center; justify-content: center; width: 1em; height: 1em; font-size: inherit; line-height: 1; } :where(viconic-icon):not(.ci-multicolor) svg { width: 100%; height: 100%; display: block; fill: currentColor; } :where(viconic-icon).ci-multicolor svg { width: 100%; height: 100%; display: block; }";
|
|
48
|
+
if (document.head.firstChild) {
|
|
49
|
+
document.head.insertBefore(style, document.head.firstChild);
|
|
50
|
+
} else {
|
|
51
|
+
document.head.appendChild(style);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const CACHE_TTL = 3 * 24 * 60 * 60 * 1e3;
|
|
55
|
+
const CACHE_PREFIX = "copyicons:svg:";
|
|
56
|
+
const ICON_CACHE_PREFIX = "ci:";
|
|
57
|
+
const PREFIXMAP_CACHE_KEY = "ci:pfx-map";
|
|
58
|
+
const PREFIXMAP_TTL = 5 * 60 * 1e3;
|
|
59
|
+
const MAX_MEMORY_CACHE_SIZE = 500;
|
|
60
|
+
const CDN_TIMEOUT = 3e3;
|
|
61
|
+
const PREFIXMAP_TIMEOUT = 2e3;
|
|
62
|
+
const API_TIMEOUT = 8e3;
|
|
63
|
+
const BUNDLE_TIMEOUT = 5e3;
|
|
64
|
+
const CDN_CONCURRENCY = 15;
|
|
65
|
+
const BUNDLE_THRESHOLD = 80;
|
|
66
|
+
const MAX_BUNDLE_CACHE = 2;
|
|
67
|
+
const MEMORY_BUDGET = 290 * 1024 * 1024;
|
|
68
|
+
const MEMORY_CHECK_INTERVAL = 3e4;
|
|
69
|
+
const bundleCache = /* @__PURE__ */ new Map();
|
|
70
|
+
const memoryCache = /* @__PURE__ */ new Map();
|
|
71
|
+
const PREFIX_TOKEN_RE = /^[a-z0-9_]{1,32}$/i;
|
|
72
|
+
const DENY_PREFIXES = /* @__PURE__ */ new Set([
|
|
73
|
+
"icon",
|
|
74
|
+
"icons",
|
|
75
|
+
"active",
|
|
76
|
+
"disabled",
|
|
77
|
+
"hidden",
|
|
78
|
+
"editor",
|
|
79
|
+
"preview",
|
|
80
|
+
"modal",
|
|
81
|
+
"btn",
|
|
82
|
+
"js",
|
|
83
|
+
"svg",
|
|
84
|
+
"text",
|
|
85
|
+
"bg",
|
|
86
|
+
"flex",
|
|
87
|
+
"inline",
|
|
88
|
+
"block",
|
|
89
|
+
"grid",
|
|
90
|
+
"gap",
|
|
91
|
+
"px",
|
|
92
|
+
"py",
|
|
93
|
+
"pt",
|
|
94
|
+
"pb",
|
|
95
|
+
"pl",
|
|
96
|
+
"pr",
|
|
97
|
+
"m",
|
|
98
|
+
"mx",
|
|
99
|
+
"my",
|
|
100
|
+
"mt",
|
|
101
|
+
"mb",
|
|
102
|
+
"ml",
|
|
103
|
+
"mr",
|
|
104
|
+
"w",
|
|
105
|
+
"h",
|
|
106
|
+
"min",
|
|
107
|
+
"max",
|
|
108
|
+
"border",
|
|
109
|
+
"rounded",
|
|
110
|
+
"shadow",
|
|
111
|
+
"opacity",
|
|
112
|
+
"hover",
|
|
113
|
+
"focus",
|
|
114
|
+
"group",
|
|
115
|
+
"transition",
|
|
116
|
+
"duration",
|
|
117
|
+
"ease",
|
|
118
|
+
"absolute",
|
|
119
|
+
"relative",
|
|
120
|
+
"fixed",
|
|
121
|
+
"sticky",
|
|
122
|
+
"top",
|
|
123
|
+
"bottom",
|
|
124
|
+
"left",
|
|
125
|
+
"right",
|
|
126
|
+
"z",
|
|
127
|
+
"overflow",
|
|
128
|
+
"truncate",
|
|
129
|
+
"translate",
|
|
130
|
+
"scale",
|
|
131
|
+
"rotate",
|
|
132
|
+
"skew",
|
|
133
|
+
"zinc",
|
|
134
|
+
"gray",
|
|
135
|
+
"slate",
|
|
136
|
+
"neutral",
|
|
137
|
+
"stone",
|
|
138
|
+
"red",
|
|
139
|
+
"orange",
|
|
140
|
+
"amber",
|
|
141
|
+
"yellow",
|
|
142
|
+
"lime",
|
|
143
|
+
"green",
|
|
144
|
+
"emerald",
|
|
145
|
+
"teal",
|
|
146
|
+
"cyan",
|
|
147
|
+
"sky",
|
|
148
|
+
"blue",
|
|
149
|
+
"indigo",
|
|
150
|
+
"violet",
|
|
151
|
+
"purple",
|
|
152
|
+
"fuchsia",
|
|
153
|
+
"pink",
|
|
154
|
+
"rose",
|
|
155
|
+
"white",
|
|
156
|
+
"black"
|
|
157
|
+
]);
|
|
158
|
+
const ICON_CLASS_PREFIXES = ["cid-", "cis-", "cim-", "cir-"];
|
|
159
|
+
function camelToKebab(str) {
|
|
160
|
+
if (!str) return "";
|
|
161
|
+
let result = str.replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").replace(/([a-z0-9])([A-Z])/g, "$1-$2");
|
|
162
|
+
while (/[A-Z]{2,}$/.test(result)) {
|
|
163
|
+
result = result.replace(/([A-Z])([A-Z])$/, "$1-$2");
|
|
164
|
+
}
|
|
165
|
+
return result.toLowerCase();
|
|
166
|
+
}
|
|
167
|
+
function kebabToCamel(str) {
|
|
168
|
+
if (!str) return "";
|
|
169
|
+
return str.replace(/-([a-zA-Z])/g, (_, c) => c.toUpperCase());
|
|
170
|
+
}
|
|
171
|
+
function parseViconicIcon(attrValue) {
|
|
172
|
+
if (!attrValue) return null;
|
|
173
|
+
const colonIdx = attrValue.indexOf(":");
|
|
174
|
+
if (colonIdx < 1) return null;
|
|
175
|
+
const prefix = attrValue.slice(0, colonIdx).trim();
|
|
176
|
+
const camelName = attrValue.slice(colonIdx + 1).trim();
|
|
177
|
+
if (!prefix || !camelName) return null;
|
|
178
|
+
return { prefix, iconName: camelToKebab(camelName) };
|
|
179
|
+
}
|
|
180
|
+
function iconClassToViconic(iconClass) {
|
|
181
|
+
if (!iconClass) return null;
|
|
182
|
+
const tokens = iconClass.trim().split(/\s+/);
|
|
183
|
+
let prefix = "", rawName = "", animate = "";
|
|
184
|
+
for (const token of tokens) {
|
|
185
|
+
if (token === "svg-loaded" || token === "js-processed" || token === "card-icon") continue;
|
|
186
|
+
if (!token.includes("-")) {
|
|
187
|
+
if (PREFIX_TOKEN_RE.test(token)) prefix = token;
|
|
188
|
+
} else {
|
|
189
|
+
let name = "";
|
|
190
|
+
let isIconPfx = false;
|
|
191
|
+
for (const iconPfx of ICON_CLASS_PREFIXES) {
|
|
192
|
+
if (token.startsWith(iconPfx)) {
|
|
193
|
+
name = token.slice(iconPfx.length);
|
|
194
|
+
isIconPfx = true;
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (isIconPfx && name) {
|
|
199
|
+
rawName = name;
|
|
200
|
+
} else if (token.startsWith("ci-")) {
|
|
201
|
+
animate = token.slice(3);
|
|
202
|
+
} else if (!isCSSUtilityClass(token)) {
|
|
203
|
+
const dashIdx = token.indexOf("-");
|
|
204
|
+
if (dashIdx > 0) rawName = token.slice(dashIdx + 1);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (!rawName) return null;
|
|
209
|
+
return { prefix, name: kebabToCamel(rawName), animate: animate || null };
|
|
210
|
+
}
|
|
211
|
+
function viconicTagFromClass(iconClass, animation) {
|
|
212
|
+
const parsed = iconClassToViconic(iconClass);
|
|
213
|
+
if (!parsed) return `<viconic-icon icon="${iconClass}"></viconic-icon>`;
|
|
214
|
+
const anim = animation || parsed.animate;
|
|
215
|
+
let tag = `<viconic-icon icon="${parsed.prefix}:${parsed.name}"`;
|
|
216
|
+
if (anim && anim !== "none") tag += ` animate="${anim}"`;
|
|
217
|
+
tag += "></viconic-icon>";
|
|
218
|
+
return tag;
|
|
219
|
+
}
|
|
220
|
+
const currentScript = document.currentScript;
|
|
221
|
+
const config = {
|
|
222
|
+
apiBase: (((_a = window.CONFIG) == null ? void 0 : _a.API_BASE) || ((_b = currentScript == null ? void 0 : currentScript.dataset) == null ? void 0 : _b.apiBase) || "https://api.viconic.io.vn").replace(/\/$/, ""),
|
|
223
|
+
// Batch API for Iconify-style loading (by-prefix endpoint)
|
|
224
|
+
batchApiBase: (((_c = window.CONFIG) == null ? void 0 : _c.BATCH_API_BASE) || ((_d = window.CONFIG) == null ? void 0 : _d.API_BASE) || ((_e = currentScript == null ? void 0 : currentScript.dataset) == null ? void 0 : _e.batchApiBase) || "https://api.viconic.io.vn").replace(/\/$/, ""),
|
|
225
|
+
cdnDomain: ((_f = currentScript == null ? void 0 : currentScript.dataset) == null ? void 0 : _f.cdnDomain) || ((_g = window.CONFIG) == null ? void 0 : _g.CDN_DOMAIN) || "pub-1f6e5603f8674e37b76ceac17e995ec3.r2.dev",
|
|
226
|
+
// Custom domain for R2 CDN via Cloudflare edge (HTTP/2, edge cache)
|
|
227
|
+
cdnCustomDomain: ((_h = window.CONFIG) == null ? void 0 : _h.CDN_CUSTOM_DOMAIN) || ((_i = currentScript == null ? void 0 : currentScript.dataset) == null ? void 0 : _i.cdnCustomDomain) || "",
|
|
228
|
+
observe: (currentScript == null ? void 0 : currentScript.hasAttribute("data-observe")) ?? false,
|
|
229
|
+
lazy: (currentScript == null ? void 0 : currentScript.hasAttribute("data-lazy")) ?? false
|
|
230
|
+
};
|
|
231
|
+
try {
|
|
232
|
+
const cdnHost = config.cdnCustomDomain || config.cdnDomain;
|
|
233
|
+
const cdnLink = document.createElement("link");
|
|
234
|
+
cdnLink.rel = "preconnect";
|
|
235
|
+
cdnLink.href = `https://${cdnHost}`;
|
|
236
|
+
cdnLink.crossOrigin = "";
|
|
237
|
+
document.head.appendChild(cdnLink);
|
|
238
|
+
const batchApiLink = document.createElement("link");
|
|
239
|
+
batchApiLink.rel = "preconnect";
|
|
240
|
+
batchApiLink.href = config.batchApiBase;
|
|
241
|
+
batchApiLink.crossOrigin = "";
|
|
242
|
+
document.head.appendChild(batchApiLink);
|
|
243
|
+
if (config.apiBase !== config.batchApiBase) {
|
|
244
|
+
const apiLink = document.createElement("link");
|
|
245
|
+
apiLink.rel = "preconnect";
|
|
246
|
+
apiLink.href = config.apiBase;
|
|
247
|
+
apiLink.crossOrigin = "";
|
|
248
|
+
document.head.appendChild(apiLink);
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {
|
|
251
|
+
}
|
|
252
|
+
let processedElements = /* @__PURE__ */ new WeakSet();
|
|
253
|
+
const pendingIcons = /* @__PURE__ */ new Map();
|
|
254
|
+
let batchTimer = null;
|
|
255
|
+
let mutationObserver = null;
|
|
256
|
+
let intersectionObserver = null;
|
|
257
|
+
let currentLoadId = 0;
|
|
258
|
+
const inflightFetches = /* @__PURE__ */ new Map();
|
|
259
|
+
const failedPrefixes = /* @__PURE__ */ new Set();
|
|
260
|
+
let prefixMap = null;
|
|
261
|
+
let prefixMapPromise = null;
|
|
262
|
+
try {
|
|
263
|
+
const cached = localStorage.getItem(PREFIXMAP_CACHE_KEY);
|
|
264
|
+
if (cached) {
|
|
265
|
+
const parsed = JSON.parse(cached);
|
|
266
|
+
if (Date.now() - parsed.t < PREFIXMAP_TTL) {
|
|
267
|
+
prefixMap = parsed.d;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} catch (e) {
|
|
271
|
+
}
|
|
272
|
+
function rewriteCdnUrl(url) {
|
|
273
|
+
if (!config.cdnCustomDomain || !url) return url;
|
|
274
|
+
return url.replace(
|
|
275
|
+
/https:\/\/pub-[a-z0-9]+\.r2\.dev/,
|
|
276
|
+
`https://${config.cdnCustomDomain}`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
function rewritePrefixMap(map) {
|
|
280
|
+
if (!config.cdnCustomDomain || !map) return map;
|
|
281
|
+
const rewritten = {};
|
|
282
|
+
for (const [key, val] of Object.entries(map)) {
|
|
283
|
+
rewritten[key] = rewriteCdnUrl(val);
|
|
284
|
+
}
|
|
285
|
+
return rewritten;
|
|
286
|
+
}
|
|
287
|
+
async function loadPrefixMap() {
|
|
288
|
+
if (prefixMap) return prefixMap;
|
|
289
|
+
if (prefixMapPromise) return prefixMapPromise;
|
|
290
|
+
prefixMapPromise = (async () => {
|
|
291
|
+
try {
|
|
292
|
+
const cdnHost = config.cdnCustomDomain || config.cdnDomain;
|
|
293
|
+
const resp = await Promise.race([
|
|
294
|
+
fetch(`https://${cdnHost}/prefix-map.json`).catch(() => null),
|
|
295
|
+
new Promise((resolve) => setTimeout(() => resolve(null), PREFIXMAP_TIMEOUT))
|
|
296
|
+
]);
|
|
297
|
+
if (resp && resp.ok) {
|
|
298
|
+
const raw = await resp.json();
|
|
299
|
+
prefixMap = rewritePrefixMap(raw);
|
|
300
|
+
try {
|
|
301
|
+
localStorage.setItem(PREFIXMAP_CACHE_KEY, JSON.stringify({
|
|
302
|
+
d: prefixMap,
|
|
303
|
+
t: Date.now()
|
|
304
|
+
}));
|
|
305
|
+
} catch (e) {
|
|
306
|
+
}
|
|
307
|
+
return prefixMap;
|
|
308
|
+
}
|
|
309
|
+
} catch (e) {
|
|
310
|
+
}
|
|
311
|
+
prefixMap = {};
|
|
312
|
+
return prefixMap;
|
|
313
|
+
})();
|
|
314
|
+
return prefixMapPromise;
|
|
315
|
+
}
|
|
316
|
+
if (prefixMap) {
|
|
317
|
+
prefixMap = rewritePrefixMap(prefixMap);
|
|
318
|
+
const bgHost = config.cdnCustomDomain || config.cdnDomain;
|
|
319
|
+
fetch(`https://${bgHost}/prefix-map.json`).then((r) => r.ok ? r.json() : null).then((data) => {
|
|
320
|
+
if (data) {
|
|
321
|
+
prefixMap = rewritePrefixMap(data);
|
|
322
|
+
try {
|
|
323
|
+
localStorage.setItem(PREFIXMAP_CACHE_KEY, JSON.stringify({
|
|
324
|
+
d: prefixMap,
|
|
325
|
+
t: Date.now()
|
|
326
|
+
}));
|
|
327
|
+
} catch (e) {
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}).catch(() => {
|
|
331
|
+
});
|
|
332
|
+
} else {
|
|
333
|
+
loadPrefixMap();
|
|
334
|
+
}
|
|
335
|
+
function getMemKey(prefix, iconName) {
|
|
336
|
+
return `${prefix}:${iconName}`;
|
|
337
|
+
}
|
|
338
|
+
function getFromMemory(prefix, iconName) {
|
|
339
|
+
const key = getMemKey(prefix, iconName);
|
|
340
|
+
const svg = memoryCache.get(key);
|
|
341
|
+
if (svg) {
|
|
342
|
+
memoryCache.delete(key);
|
|
343
|
+
memoryCache.set(key, svg);
|
|
344
|
+
}
|
|
345
|
+
return svg;
|
|
346
|
+
}
|
|
347
|
+
function setToMemory(prefix, iconName, svg) {
|
|
348
|
+
const key = getMemKey(prefix, iconName);
|
|
349
|
+
if (memoryCache.has(key)) {
|
|
350
|
+
memoryCache.delete(key);
|
|
351
|
+
} else if (memoryCache.size >= MAX_MEMORY_CACHE_SIZE) {
|
|
352
|
+
const deleteCount = Math.floor(MAX_MEMORY_CACHE_SIZE * 0.5);
|
|
353
|
+
const keysToDelete = [...memoryCache.keys()].slice(0, deleteCount);
|
|
354
|
+
for (const k of keysToDelete) memoryCache.delete(k);
|
|
355
|
+
}
|
|
356
|
+
memoryCache.set(key, svg);
|
|
357
|
+
}
|
|
358
|
+
function getPerIconCache(prefix, iconName) {
|
|
359
|
+
const memCached = getFromMemory(prefix, iconName);
|
|
360
|
+
if (memCached && typeof memCached === "string") return memCached;
|
|
361
|
+
try {
|
|
362
|
+
const key = `${ICON_CACHE_PREFIX}${prefix}:${iconName}`;
|
|
363
|
+
const cached = localStorage.getItem(key);
|
|
364
|
+
if (cached) {
|
|
365
|
+
const { s: svg, t: timestamp } = JSON.parse(cached);
|
|
366
|
+
if (typeof svg === "string" && svg.trim() && Date.now() - timestamp < CACHE_TTL) {
|
|
367
|
+
setToMemory(prefix, iconName, svg);
|
|
368
|
+
return svg;
|
|
369
|
+
}
|
|
370
|
+
localStorage.removeItem(key);
|
|
371
|
+
}
|
|
372
|
+
} catch (e) {
|
|
373
|
+
}
|
|
374
|
+
return null;
|
|
375
|
+
}
|
|
376
|
+
const _deferredWrites = [];
|
|
377
|
+
let _deferTimer = null;
|
|
378
|
+
function deferCacheWrite(prefix, iconName, svg) {
|
|
379
|
+
if (typeof svg !== "string" || !svg.trim()) return;
|
|
380
|
+
_deferredWrites.push({ prefix, iconName, svg });
|
|
381
|
+
if (!_deferTimer) {
|
|
382
|
+
if ("requestIdleCallback" in window) {
|
|
383
|
+
_deferTimer = requestIdleCallback(_flushCacheWrites, { timeout: 2e3 });
|
|
384
|
+
} else {
|
|
385
|
+
_deferTimer = setTimeout(_flushCacheWrites, 100);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function _flushCacheWrites() {
|
|
390
|
+
_deferTimer = null;
|
|
391
|
+
const writes = _deferredWrites.splice(0);
|
|
392
|
+
const now = Date.now();
|
|
393
|
+
for (const { prefix, iconName, svg } of writes) {
|
|
394
|
+
try {
|
|
395
|
+
const key = `${ICON_CACHE_PREFIX}${prefix}:${iconName}`;
|
|
396
|
+
localStorage.setItem(key, JSON.stringify({ s: svg, t: now }));
|
|
397
|
+
} catch (e) {
|
|
398
|
+
cleanupOldCache();
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function setPerIconCache(prefix, iconName, svg) {
|
|
404
|
+
if (typeof svg !== "string" || !svg.trim()) return;
|
|
405
|
+
setToMemory(prefix, iconName, svg);
|
|
406
|
+
deferCacheWrite(prefix, iconName, svg);
|
|
407
|
+
}
|
|
408
|
+
function getCachedData(key) {
|
|
409
|
+
try {
|
|
410
|
+
const cached = localStorage.getItem(CACHE_PREFIX + key);
|
|
411
|
+
if (cached) {
|
|
412
|
+
const { data, timestamp } = JSON.parse(cached);
|
|
413
|
+
if (Date.now() - timestamp < CACHE_TTL) return data;
|
|
414
|
+
localStorage.removeItem(CACHE_PREFIX + key);
|
|
415
|
+
}
|
|
416
|
+
} catch (e) {
|
|
417
|
+
}
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
function setCachedData(key, data) {
|
|
421
|
+
try {
|
|
422
|
+
localStorage.setItem(CACHE_PREFIX + key, JSON.stringify({
|
|
423
|
+
data,
|
|
424
|
+
timestamp: Date.now()
|
|
425
|
+
}));
|
|
426
|
+
} catch (e) {
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
function cleanupOldCache() {
|
|
430
|
+
try {
|
|
431
|
+
const keysToRemove = [];
|
|
432
|
+
const now = Date.now();
|
|
433
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
434
|
+
const key = localStorage.key(i);
|
|
435
|
+
if (!key) continue;
|
|
436
|
+
if (key.startsWith(ICON_CACHE_PREFIX) || key.startsWith(CACHE_PREFIX)) {
|
|
437
|
+
try {
|
|
438
|
+
const cached = localStorage.getItem(key);
|
|
439
|
+
if (cached) {
|
|
440
|
+
const parsed = JSON.parse(cached);
|
|
441
|
+
const timestamp = parsed.t || parsed.timestamp;
|
|
442
|
+
if (now - timestamp > CACHE_TTL) keysToRemove.push(key);
|
|
443
|
+
}
|
|
444
|
+
} catch (e) {
|
|
445
|
+
keysToRemove.push(key);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
keysToRemove.forEach((k) => localStorage.removeItem(k));
|
|
450
|
+
} catch (e) {
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function purgeCorruptedCache() {
|
|
454
|
+
const PURGE_KEY = "ci:purged_v2";
|
|
455
|
+
if (localStorage.getItem(PURGE_KEY)) return;
|
|
456
|
+
try {
|
|
457
|
+
const keysToRemove = [];
|
|
458
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
459
|
+
const key = localStorage.key(i);
|
|
460
|
+
if (!key || !key.startsWith(ICON_CACHE_PREFIX)) continue;
|
|
461
|
+
try {
|
|
462
|
+
const cached = localStorage.getItem(key);
|
|
463
|
+
if (!cached) continue;
|
|
464
|
+
const parsed = JSON.parse(cached);
|
|
465
|
+
if (typeof parsed.s !== "string" || parsed.s.includes("[object") || !parsed.s.trim()) {
|
|
466
|
+
keysToRemove.push(key);
|
|
467
|
+
}
|
|
468
|
+
} catch (e) {
|
|
469
|
+
keysToRemove.push(key);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
keysToRemove.forEach((k) => localStorage.removeItem(k));
|
|
473
|
+
localStorage.setItem(PURGE_KEY, "1");
|
|
474
|
+
} catch (e) {
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
purgeCorruptedCache();
|
|
478
|
+
function detectPrefix(iconEl) {
|
|
479
|
+
const dataCollection = iconEl.getAttribute("data-collection");
|
|
480
|
+
if (dataCollection) return dataCollection;
|
|
481
|
+
const cls = iconEl.classList;
|
|
482
|
+
if (!cls || cls.length === 0) return null;
|
|
483
|
+
let hasIconClass = false;
|
|
484
|
+
let prefix = null;
|
|
485
|
+
for (let i = 0; i < cls.length; i++) {
|
|
486
|
+
const token = cls[i];
|
|
487
|
+
if (!token) continue;
|
|
488
|
+
if (token.includes("-")) {
|
|
489
|
+
hasIconClass = true;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (!PREFIX_TOKEN_RE.test(token)) continue;
|
|
493
|
+
if (DENY_PREFIXES.has(token.toLowerCase())) continue;
|
|
494
|
+
prefix = token;
|
|
495
|
+
}
|
|
496
|
+
return hasIconClass ? prefix : null;
|
|
497
|
+
}
|
|
498
|
+
function extractIconName(className, dataCollection) {
|
|
499
|
+
for (const iconPrefix of ICON_CLASS_PREFIXES) {
|
|
500
|
+
if (className.startsWith(iconPrefix)) {
|
|
501
|
+
return className.slice(iconPrefix.length) || null;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (dataCollection) {
|
|
505
|
+
const colPrefix = dataCollection.endsWith("-") ? dataCollection : dataCollection + "-";
|
|
506
|
+
if (className.startsWith(colPrefix)) {
|
|
507
|
+
return className.slice(colPrefix.length) || null;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
const dashIndex = className.indexOf("-");
|
|
511
|
+
if (dashIndex > 0) {
|
|
512
|
+
return className.slice(dashIndex + 1);
|
|
513
|
+
}
|
|
514
|
+
return null;
|
|
515
|
+
}
|
|
516
|
+
const STYLE_SUFFIXES = /* @__PURE__ */ new Set([
|
|
517
|
+
"solid",
|
|
518
|
+
"regular",
|
|
519
|
+
"brands",
|
|
520
|
+
"light",
|
|
521
|
+
"thin",
|
|
522
|
+
"duotone",
|
|
523
|
+
"sharp",
|
|
524
|
+
"outlined",
|
|
525
|
+
"round",
|
|
526
|
+
"two-tone",
|
|
527
|
+
"filled",
|
|
528
|
+
"line",
|
|
529
|
+
"bulk",
|
|
530
|
+
"broken",
|
|
531
|
+
"linear",
|
|
532
|
+
"twotone",
|
|
533
|
+
"outline",
|
|
534
|
+
"fill"
|
|
535
|
+
]);
|
|
536
|
+
const CSS_UTILITY_PREFIXES = [
|
|
537
|
+
"text-",
|
|
538
|
+
"bg-",
|
|
539
|
+
"w-",
|
|
540
|
+
"h-",
|
|
541
|
+
"p-",
|
|
542
|
+
"m-",
|
|
543
|
+
"px-",
|
|
544
|
+
"py-",
|
|
545
|
+
"pt-",
|
|
546
|
+
"pb-",
|
|
547
|
+
"pl-",
|
|
548
|
+
"pr-",
|
|
549
|
+
"mx-",
|
|
550
|
+
"my-",
|
|
551
|
+
"mt-",
|
|
552
|
+
"mb-",
|
|
553
|
+
"ml-",
|
|
554
|
+
"mr-",
|
|
555
|
+
"gap-",
|
|
556
|
+
"space-",
|
|
557
|
+
"flex-",
|
|
558
|
+
"grid-",
|
|
559
|
+
"col-",
|
|
560
|
+
"row-",
|
|
561
|
+
"border-",
|
|
562
|
+
"rounded-",
|
|
563
|
+
"shadow-",
|
|
564
|
+
"opacity-",
|
|
565
|
+
"z-",
|
|
566
|
+
"top-",
|
|
567
|
+
"bottom-",
|
|
568
|
+
"left-",
|
|
569
|
+
"right-",
|
|
570
|
+
"inset-",
|
|
571
|
+
"min-",
|
|
572
|
+
"max-",
|
|
573
|
+
"overflow-",
|
|
574
|
+
"translate-",
|
|
575
|
+
"scale-",
|
|
576
|
+
"rotate-",
|
|
577
|
+
"skew-",
|
|
578
|
+
"transition-",
|
|
579
|
+
"duration-",
|
|
580
|
+
"ease-",
|
|
581
|
+
"delay-",
|
|
582
|
+
"animate-",
|
|
583
|
+
"font-",
|
|
584
|
+
"leading-",
|
|
585
|
+
"tracking-",
|
|
586
|
+
"decoration-",
|
|
587
|
+
"underline-",
|
|
588
|
+
"line-",
|
|
589
|
+
"hover:",
|
|
590
|
+
"focus:",
|
|
591
|
+
"active:",
|
|
592
|
+
"group-",
|
|
593
|
+
"dark:",
|
|
594
|
+
"sm:",
|
|
595
|
+
"md:",
|
|
596
|
+
"lg:",
|
|
597
|
+
"xl:",
|
|
598
|
+
"from-",
|
|
599
|
+
"to-",
|
|
600
|
+
"via-",
|
|
601
|
+
"ring-",
|
|
602
|
+
"divide-",
|
|
603
|
+
"place-",
|
|
604
|
+
"items-",
|
|
605
|
+
"justify-",
|
|
606
|
+
"self-",
|
|
607
|
+
"order-",
|
|
608
|
+
"cursor-",
|
|
609
|
+
"select-",
|
|
610
|
+
"resize-",
|
|
611
|
+
"fill-",
|
|
612
|
+
"stroke-",
|
|
613
|
+
"object-",
|
|
614
|
+
"aspect-",
|
|
615
|
+
"columns-",
|
|
616
|
+
"break-",
|
|
617
|
+
"box-",
|
|
618
|
+
"float-",
|
|
619
|
+
"clear-",
|
|
620
|
+
"isolation-",
|
|
621
|
+
"overscroll-",
|
|
622
|
+
"scroll-",
|
|
623
|
+
"snap-",
|
|
624
|
+
"touch-",
|
|
625
|
+
"appearance-",
|
|
626
|
+
"outline-",
|
|
627
|
+
"accent-",
|
|
628
|
+
"caret-",
|
|
629
|
+
"will-",
|
|
630
|
+
"contain-",
|
|
631
|
+
"content-",
|
|
632
|
+
"data-",
|
|
633
|
+
"nav-",
|
|
634
|
+
"card-",
|
|
635
|
+
"btn-",
|
|
636
|
+
"modal-",
|
|
637
|
+
"sidebar-",
|
|
638
|
+
"editor-",
|
|
639
|
+
"hero-",
|
|
640
|
+
"mobile-",
|
|
641
|
+
"search-",
|
|
642
|
+
"pagination-",
|
|
643
|
+
"collection-",
|
|
644
|
+
"explore-",
|
|
645
|
+
"svg-",
|
|
646
|
+
"trending-",
|
|
647
|
+
"png-",
|
|
648
|
+
"kit-",
|
|
649
|
+
"color-",
|
|
650
|
+
"section-",
|
|
651
|
+
"toolbar-",
|
|
652
|
+
"-translate-",
|
|
653
|
+
"-mt-",
|
|
654
|
+
"-mb-",
|
|
655
|
+
"-ml-",
|
|
656
|
+
"-mr-"
|
|
657
|
+
];
|
|
658
|
+
function isCSSUtilityClass(token) {
|
|
659
|
+
const lower = token.toLowerCase();
|
|
660
|
+
for (const prefix of CSS_UTILITY_PREFIXES) {
|
|
661
|
+
if (lower.startsWith(prefix)) return true;
|
|
662
|
+
}
|
|
663
|
+
if (lower.includes(":")) return true;
|
|
664
|
+
return false;
|
|
665
|
+
}
|
|
666
|
+
function getIconNames(iconEl) {
|
|
667
|
+
const knownPrefixNames = [];
|
|
668
|
+
const otherNames = [];
|
|
669
|
+
const cls = iconEl.classList;
|
|
670
|
+
const dataCollection = iconEl.getAttribute("data-collection") || "";
|
|
671
|
+
for (let i = 0; i < cls.length; i++) {
|
|
672
|
+
const token = cls[i];
|
|
673
|
+
if (!token || !token.includes("-")) continue;
|
|
674
|
+
if (isCSSUtilityClass(token)) continue;
|
|
675
|
+
let isKnownPrefix = false;
|
|
676
|
+
for (const iconPrefix of ICON_CLASS_PREFIXES) {
|
|
677
|
+
if (token.startsWith(iconPrefix)) {
|
|
678
|
+
isKnownPrefix = true;
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const name = extractIconName(token, dataCollection);
|
|
683
|
+
if (name) {
|
|
684
|
+
if (STYLE_SUFFIXES.has(name.toLowerCase())) continue;
|
|
685
|
+
if (isKnownPrefix) {
|
|
686
|
+
knownPrefixNames.push(name);
|
|
687
|
+
} else {
|
|
688
|
+
otherNames.push(name);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return knownPrefixNames.length > 0 ? knownPrefixNames : otherNames;
|
|
693
|
+
}
|
|
694
|
+
function isInDOM(element) {
|
|
695
|
+
return element && element.isConnected !== false && document.body.contains(element);
|
|
696
|
+
}
|
|
697
|
+
function isMultiColorSvg(svgString) {
|
|
698
|
+
function normalizeColor(c) {
|
|
699
|
+
if (!c) return null;
|
|
700
|
+
c = c.trim().toLowerCase();
|
|
701
|
+
if (c === "none" || c === "inherit" || c === "transparent" || c === "currentcolor") return null;
|
|
702
|
+
if (c === "#000" || c === "#000000" || c === "black" || c === "rgb(0,0,0)" || c === "rgb(0, 0, 0)") return "#000000";
|
|
703
|
+
if (c === "#fff" || c === "#ffffff" || c === "white" || c === "rgb(255,255,255)" || c === "rgb(255, 255, 255)") return "#ffffff";
|
|
704
|
+
const hex3 = c.match(/^#([0-9a-f])([0-9a-f])([0-9a-f])$/);
|
|
705
|
+
if (hex3) return "#" + hex3[1] + hex3[1] + hex3[2] + hex3[2] + hex3[3] + hex3[3];
|
|
706
|
+
return c;
|
|
707
|
+
}
|
|
708
|
+
const colors = /* @__PURE__ */ new Set();
|
|
709
|
+
const attrRe = /(?:fill|stroke)\s*=\s*"([^"]+)"/gi;
|
|
710
|
+
let m;
|
|
711
|
+
while ((m = attrRe.exec(svgString)) !== null) {
|
|
712
|
+
const nc = normalizeColor(m[1]);
|
|
713
|
+
if (nc) colors.add(nc);
|
|
714
|
+
}
|
|
715
|
+
const styleAttrRe = /style\s*=\s*"([^"]+)"/gi;
|
|
716
|
+
while ((m = styleAttrRe.exec(svgString)) !== null) {
|
|
717
|
+
const styleStr = m[1];
|
|
718
|
+
const propRe = /(?:fill|stroke)\s*:\s*([^;"\s]+)/gi;
|
|
719
|
+
let pm;
|
|
720
|
+
while ((pm = propRe.exec(styleStr)) !== null) {
|
|
721
|
+
const nc = normalizeColor(pm[1]);
|
|
722
|
+
if (nc) colors.add(nc);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const styleBlockRe = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
|
726
|
+
while ((m = styleBlockRe.exec(svgString)) !== null) {
|
|
727
|
+
const css = m[1];
|
|
728
|
+
const cssPropRe = /(?:fill|stroke)\s*:\s*([^;}\s]+)/gi;
|
|
729
|
+
let cm;
|
|
730
|
+
while ((cm = cssPropRe.exec(css)) !== null) {
|
|
731
|
+
const nc = normalizeColor(cm[1]);
|
|
732
|
+
if (nc) colors.add(nc);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
if (colors.size >= 2) return true;
|
|
736
|
+
if (colors.size === 1 && !colors.has("#000000")) return true;
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
function injectSVG(element, svgContent) {
|
|
740
|
+
if (!isInDOM(element)) return false;
|
|
741
|
+
if (element.classList.contains("svg-loaded")) return true;
|
|
742
|
+
if (typeof svgContent !== "string" || !svgContent.trim()) return false;
|
|
743
|
+
const multiColor = isMultiColorSvg(svgContent);
|
|
744
|
+
let scopedContent = svgContent;
|
|
745
|
+
if (multiColor) {
|
|
746
|
+
scopedContent = scopedContent.replace(/((?:fill|stroke)\s*=\s*")currentColor(")/gi, "$1#000$2");
|
|
747
|
+
scopedContent = scopedContent.replace(/((?:fill|stroke)\s*:\s*)currentColor/gi, "$1#000");
|
|
748
|
+
}
|
|
749
|
+
if (scopedContent.includes("<style") || scopedContent.includes("id=")) {
|
|
750
|
+
const uid = "ci" + Math.random().toString(36).slice(2, 7);
|
|
751
|
+
if (scopedContent.includes("<style")) {
|
|
752
|
+
scopedContent = scopedContent.replace(/<style[^>]*>([\s\S]*?)<\/style>/gi, (match, css) => {
|
|
753
|
+
return match.replace(css, css.replace(/\.([a-zA-Z0-9_\-]+)/g, "." + uid + "-$1"));
|
|
754
|
+
});
|
|
755
|
+
scopedContent = scopedContent.replace(/class="([^"]+)"/gi, (match, classes) => {
|
|
756
|
+
if (classes.includes("svg-loaded")) return match;
|
|
757
|
+
return 'class="' + classes.split(/\s+/).map((c) => c ? uid + "-" + c : "").join(" ") + '"';
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
if (scopedContent.includes("id=")) {
|
|
761
|
+
scopedContent = scopedContent.replace(/id="([^"]+)"/gi, `id="${uid}-$1"`);
|
|
762
|
+
scopedContent = scopedContent.replace(/url\(['"]?#([^'"\)]+)['"]?\)/gi, `url(#${uid}-$1)`);
|
|
763
|
+
scopedContent = scopedContent.replace(/href="#([^"]+)"/gi, `href="#${uid}-$1"`);
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
element.innerHTML = scopedContent;
|
|
767
|
+
element.classList.add("svg-loaded");
|
|
768
|
+
if (multiColor) {
|
|
769
|
+
element.classList.add("ci-multicolor");
|
|
770
|
+
}
|
|
771
|
+
const svg = element.querySelector("svg");
|
|
772
|
+
if (svg) {
|
|
773
|
+
const originalFill = svg.getAttribute("fill");
|
|
774
|
+
if (multiColor) {
|
|
775
|
+
svg.style.cssText = "width:1em;height:1em;display:inline-block;vertical-align:middle;";
|
|
776
|
+
} else {
|
|
777
|
+
const fillStyle = originalFill === "none" ? "none" : "currentColor";
|
|
778
|
+
svg.style.cssText = `width:1em;height:1em;fill:${fillStyle};display:inline-block;vertical-align:middle;`;
|
|
779
|
+
}
|
|
780
|
+
if (element.getAttribute("animate") === "trace") {
|
|
781
|
+
const isStrokeBased = originalFill === "none";
|
|
782
|
+
svg.style.setProperty("--trace-end-stroke", isStrokeBased ? "currentColor" : "transparent");
|
|
783
|
+
svg.style.setProperty("--trace-end-stroke-width", isStrokeBased ? svg.getAttribute("stroke-width") || "2px" : "0px");
|
|
784
|
+
svg.style.setProperty("--trace-stroke-width", isStrokeBased ? svg.getAttribute("stroke-width") || "2px" : "1px");
|
|
785
|
+
const paths = svg.querySelectorAll("path, ellipse, circle, rect, polygon, polyline");
|
|
786
|
+
for (let i = 0; i < paths.length; i++) {
|
|
787
|
+
const p = paths[i];
|
|
788
|
+
const len = typeof p.getTotalLength === "function" ? Math.ceil(p.getTotalLength()) : 400;
|
|
789
|
+
p.style.setProperty("--path-len", len);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return true;
|
|
794
|
+
}
|
|
795
|
+
function tryInjectFromCache(iconEl, prefix, iconName) {
|
|
796
|
+
const svg = getFromMemory(prefix, iconName) || getPerIconCache(prefix, iconName);
|
|
797
|
+
if (svg) {
|
|
798
|
+
iconEl.classList.add("cache-hit");
|
|
799
|
+
injectSVG(iconEl, svg);
|
|
800
|
+
return true;
|
|
801
|
+
}
|
|
802
|
+
return false;
|
|
803
|
+
}
|
|
804
|
+
function registerViconicIcon(el) {
|
|
805
|
+
if (!el || el.nodeType !== 1) return;
|
|
806
|
+
if (processedElements.has(el)) return;
|
|
807
|
+
processedElements.add(el);
|
|
808
|
+
const iconAttr = el.getAttribute("icon");
|
|
809
|
+
const parsed = parseViconicIcon(iconAttr);
|
|
810
|
+
if (!parsed) {
|
|
811
|
+
if (iconAttr) console.warn(`[CopyIcons] \u26A0\uFE0F Cannot parse viconic-icon icon="${iconAttr}"`);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
const { prefix, iconName } = parsed;
|
|
815
|
+
if (tryInjectFromCache(el, prefix, iconName)) {
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if (!pendingIcons.has(prefix)) {
|
|
819
|
+
pendingIcons.set(prefix, []);
|
|
820
|
+
}
|
|
821
|
+
pendingIcons.get(prefix).push({ element: el, iconName });
|
|
822
|
+
scheduleBatchLoad();
|
|
823
|
+
}
|
|
824
|
+
function registerIcon(iconEl) {
|
|
825
|
+
if (!iconEl || iconEl.nodeType !== 1) return;
|
|
826
|
+
if (processedElements.has(iconEl)) return;
|
|
827
|
+
processedElements.add(iconEl);
|
|
828
|
+
const prefix = detectPrefix(iconEl);
|
|
829
|
+
if (!prefix) return;
|
|
830
|
+
const iconNames = getIconNames(iconEl);
|
|
831
|
+
if (iconNames.length === 0) return;
|
|
832
|
+
const iconName = iconNames[0];
|
|
833
|
+
if (tryInjectFromCache(iconEl, prefix, iconName)) {
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (!pendingIcons.has(prefix)) {
|
|
837
|
+
pendingIcons.set(prefix, []);
|
|
838
|
+
}
|
|
839
|
+
pendingIcons.get(prefix).push({ element: iconEl, iconName });
|
|
840
|
+
scheduleBatchLoad();
|
|
841
|
+
}
|
|
842
|
+
function scheduleBatchLoad() {
|
|
843
|
+
if (batchTimer) return;
|
|
844
|
+
batchTimer = true;
|
|
845
|
+
setTimeout(() => {
|
|
846
|
+
batchTimer = null;
|
|
847
|
+
loadPendingSVGs();
|
|
848
|
+
}, 0);
|
|
849
|
+
}
|
|
850
|
+
function clearPending() {
|
|
851
|
+
if (batchTimer) {
|
|
852
|
+
clearTimeout(batchTimer);
|
|
853
|
+
batchTimer = null;
|
|
854
|
+
}
|
|
855
|
+
pendingIcons.clear();
|
|
856
|
+
processedElements = /* @__PURE__ */ new WeakSet();
|
|
857
|
+
currentLoadId++;
|
|
858
|
+
inflightFetches.clear();
|
|
859
|
+
mutationBatch.length = 0;
|
|
860
|
+
if (mutationBatchTimer) {
|
|
861
|
+
clearTimeout(mutationBatchTimer);
|
|
862
|
+
mutationBatchTimer = null;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
async function loadPendingSVGs() {
|
|
866
|
+
if (pendingIcons.size === 0) return;
|
|
867
|
+
const loadId = ++currentLoadId;
|
|
868
|
+
const groups = /* @__PURE__ */ new Map();
|
|
869
|
+
for (const [prefix, iconList] of pendingIcons) {
|
|
870
|
+
const iconNames = [...new Set(iconList.map((i) => i.iconName))];
|
|
871
|
+
groups.set(prefix, { iconNames, iconList });
|
|
872
|
+
}
|
|
873
|
+
pendingIcons.clear();
|
|
874
|
+
const uncachedGroups = /* @__PURE__ */ new Map();
|
|
875
|
+
let totalUncached = 0;
|
|
876
|
+
for (const [prefix, { iconNames, iconList }] of groups) {
|
|
877
|
+
if (failedPrefixes.has(prefix)) continue;
|
|
878
|
+
const elementMap = /* @__PURE__ */ new Map();
|
|
879
|
+
for (const { element, iconName } of iconList) {
|
|
880
|
+
if (!elementMap.has(iconName)) elementMap.set(iconName, []);
|
|
881
|
+
elementMap.get(iconName).push(element);
|
|
882
|
+
}
|
|
883
|
+
const uncachedNames = [];
|
|
884
|
+
for (const iconName of iconNames) {
|
|
885
|
+
const svg = getFromMemory(prefix, iconName) || getPerIconCache(prefix, iconName);
|
|
886
|
+
if (svg) {
|
|
887
|
+
const elements = elementMap.get(iconName);
|
|
888
|
+
if (elements) for (const el of elements) {
|
|
889
|
+
el.classList.add("cache-hit");
|
|
890
|
+
injectSVG(el, svg);
|
|
891
|
+
}
|
|
892
|
+
} else {
|
|
893
|
+
uncachedNames.push(iconName);
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
if (uncachedNames.length === 0) continue;
|
|
897
|
+
uncachedGroups.set(prefix, { uncachedNames, elementMap });
|
|
898
|
+
totalUncached += uncachedNames.length;
|
|
899
|
+
}
|
|
900
|
+
if (totalUncached === 0 || loadId !== currentLoadId) return;
|
|
901
|
+
const t0 = performance.now();
|
|
902
|
+
const map = await loadPrefixMap();
|
|
903
|
+
if (loadId !== currentLoadId) return;
|
|
904
|
+
const bundlePromises = [];
|
|
905
|
+
const cdnPromises = [];
|
|
906
|
+
const unmappedIcons = [];
|
|
907
|
+
let cdnCount = 0;
|
|
908
|
+
for (const [prefix, { uncachedNames, elementMap }] of uncachedGroups) {
|
|
909
|
+
const cdnBase = map == null ? void 0 : map[prefix];
|
|
910
|
+
if (!cdnBase) {
|
|
911
|
+
unmappedIcons.push({ prefix, uncachedNames, elementMap });
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
if (uncachedNames.length >= BUNDLE_THRESHOLD) {
|
|
915
|
+
bundlePromises.push(
|
|
916
|
+
loadIconsFromBundle(prefix, cdnBase, uncachedNames, elementMap).catch(() => {
|
|
917
|
+
for (const name of uncachedNames) {
|
|
918
|
+
cdnPromises.push(fetchAndInjectCDN(prefix, name, cdnBase, elementMap));
|
|
919
|
+
}
|
|
920
|
+
})
|
|
921
|
+
);
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
for (const name of uncachedNames) {
|
|
925
|
+
cdnCount++;
|
|
926
|
+
cdnPromises.push(fetchAndInjectCDN(prefix, name, cdnBase, elementMap));
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
async function fetchAndInjectCDN(prefix, name, cdnBase, elementMap) {
|
|
930
|
+
if (getFromMemory(prefix, name)) {
|
|
931
|
+
const els = elementMap.get(name);
|
|
932
|
+
if (els) for (const el of els) injectSVG(el, getFromMemory(prefix, name));
|
|
933
|
+
return { ok: true, prefix, name };
|
|
934
|
+
}
|
|
935
|
+
try {
|
|
936
|
+
const resp = await Promise.race([
|
|
937
|
+
fetch(`${cdnBase}/${name}.svg`).catch(() => null),
|
|
938
|
+
new Promise((r) => setTimeout(() => r(null), CDN_TIMEOUT))
|
|
939
|
+
]);
|
|
940
|
+
if (!resp || !resp.ok) return { ok: false, prefix, name };
|
|
941
|
+
const svg = await resp.text();
|
|
942
|
+
if (!svg || !svg.includes("<")) return { ok: false, prefix, name };
|
|
943
|
+
setToMemory(prefix, name, svg);
|
|
944
|
+
deferCacheWrite(prefix, name, svg);
|
|
945
|
+
const els = elementMap.get(name);
|
|
946
|
+
if (els) for (const el of els) injectSVG(el, svg);
|
|
947
|
+
return { ok: true, prefix, name };
|
|
948
|
+
} catch (e) {
|
|
949
|
+
return { ok: false, prefix, name };
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
const cdnResults = await Promise.allSettled([...bundlePromises, ...cdnPromises]);
|
|
953
|
+
const failedByPrefix = /* @__PURE__ */ new Map();
|
|
954
|
+
for (const result of cdnResults) {
|
|
955
|
+
const val = result.status === "fulfilled" ? result.value : null;
|
|
956
|
+
if (val && !val.ok && val.prefix && val.name) {
|
|
957
|
+
if (!failedByPrefix.has(val.prefix)) failedByPrefix.set(val.prefix, []);
|
|
958
|
+
failedByPrefix.get(val.prefix).push(val.name);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
for (const task of unmappedIcons) {
|
|
962
|
+
if (!failedByPrefix.has(task.prefix)) failedByPrefix.set(task.prefix, []);
|
|
963
|
+
failedByPrefix.get(task.prefix).push(...task.uncachedNames);
|
|
964
|
+
}
|
|
965
|
+
const allElementMaps = /* @__PURE__ */ new Map();
|
|
966
|
+
for (const [prefix, { elementMap }] of uncachedGroups) {
|
|
967
|
+
allElementMaps.set(prefix, elementMap);
|
|
968
|
+
}
|
|
969
|
+
for (const task of unmappedIcons) {
|
|
970
|
+
allElementMaps.set(task.prefix, task.elementMap);
|
|
971
|
+
}
|
|
972
|
+
if (failedByPrefix.size > 0 && loadId === currentLoadId) {
|
|
973
|
+
const q = [...failedByPrefix.entries()].map(([prefix, names]) => `${prefix}:${names.join(",")}`).join("|");
|
|
974
|
+
try {
|
|
975
|
+
const resp = await Promise.race([
|
|
976
|
+
fetch(`${config.batchApiBase}/api/v1/subset/multi-prefix/?q=${encodeURIComponent(q)}`).catch(() => null),
|
|
977
|
+
new Promise((r) => setTimeout(() => r(null), API_TIMEOUT))
|
|
978
|
+
]);
|
|
979
|
+
if (resp == null ? void 0 : resp.ok) {
|
|
980
|
+
const data = await resp.json().catch(() => null);
|
|
981
|
+
if (data == null ? void 0 : data.results) {
|
|
982
|
+
for (const [prefix, names] of failedByPrefix) {
|
|
983
|
+
const prefixResult = data.results[prefix];
|
|
984
|
+
if (!prefixResult) continue;
|
|
985
|
+
const elementMap = allElementMaps.get(prefix);
|
|
986
|
+
for (const name of names) {
|
|
987
|
+
const iconData = prefixResult[name];
|
|
988
|
+
const svg = typeof iconData === "string" ? iconData : iconData == null ? void 0 : iconData.svg;
|
|
989
|
+
if (!svg || typeof svg !== "string" || !svg.includes("<")) continue;
|
|
990
|
+
setToMemory(prefix, name, svg);
|
|
991
|
+
deferCacheWrite(prefix, name, svg);
|
|
992
|
+
if (elementMap) {
|
|
993
|
+
const els = elementMap.get(name);
|
|
994
|
+
if (els) for (const el of els) injectSVG(el, svg);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
} catch (e) {
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
const elapsed = Math.round(performance.now() - t0);
|
|
1004
|
+
const unmappedCount = unmappedIcons.reduce((sum, g) => sum + g.uncachedNames.length, 0);
|
|
1005
|
+
const info = unmappedCount > 0 ? ` (${cdnCount} CDN + ${unmappedCount} API)` : "";
|
|
1006
|
+
console.log(`[CopyIcons] \u26A1 ${totalUncached} icons in ${elapsed}ms${info}`);
|
|
1007
|
+
}
|
|
1008
|
+
const bundleInflight = /* @__PURE__ */ new Map();
|
|
1009
|
+
function getCollectionId(cdnBase) {
|
|
1010
|
+
if (!cdnBase) return null;
|
|
1011
|
+
const parts = cdnBase.split("/icons/");
|
|
1012
|
+
return parts.length > 1 ? parts[parts.length - 1] : null;
|
|
1013
|
+
}
|
|
1014
|
+
async function fetchBundleFromCDN(collectionId) {
|
|
1015
|
+
const cached = bundleCache.get(collectionId);
|
|
1016
|
+
if (cached) {
|
|
1017
|
+
cached.accessTime = Date.now();
|
|
1018
|
+
return cached.data;
|
|
1019
|
+
}
|
|
1020
|
+
if (bundleInflight.has(collectionId)) return bundleInflight.get(collectionId);
|
|
1021
|
+
const promise = (async () => {
|
|
1022
|
+
const urls = [
|
|
1023
|
+
`https://${config.cdnDomain}/collections/${collectionId}/icons_svg.json.gz`,
|
|
1024
|
+
`https://${config.cdnDomain}/collections/${collectionId}/icons_svg.json`
|
|
1025
|
+
];
|
|
1026
|
+
for (const url of urls) {
|
|
1027
|
+
try {
|
|
1028
|
+
const resp = await Promise.race([
|
|
1029
|
+
fetch(url).catch(() => null),
|
|
1030
|
+
new Promise((resolve) => setTimeout(() => resolve(null), BUNDLE_TIMEOUT))
|
|
1031
|
+
]);
|
|
1032
|
+
if (!resp || !resp.ok) continue;
|
|
1033
|
+
const data = await resp.json().catch(() => null);
|
|
1034
|
+
if (data && typeof data === "object" && Object.keys(data).length > 0) {
|
|
1035
|
+
const iconCount = Object.keys(data).length;
|
|
1036
|
+
const estimatedSize = iconCount * 500;
|
|
1037
|
+
enforceBundleCacheLimit();
|
|
1038
|
+
bundleCache.set(collectionId, {
|
|
1039
|
+
data,
|
|
1040
|
+
accessTime: Date.now(),
|
|
1041
|
+
size: estimatedSize,
|
|
1042
|
+
iconCount
|
|
1043
|
+
});
|
|
1044
|
+
console.log(`[CopyIcons] \u{1F4E6} Bundle loaded: ${collectionId} (${iconCount} icons from CDN, ~${Math.round(estimatedSize / 1024)}KB)`);
|
|
1045
|
+
return data;
|
|
1046
|
+
}
|
|
1047
|
+
} catch (e) {
|
|
1048
|
+
continue;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return null;
|
|
1052
|
+
})();
|
|
1053
|
+
bundleInflight.set(collectionId, promise);
|
|
1054
|
+
promise.finally(() => bundleInflight.delete(collectionId));
|
|
1055
|
+
return promise;
|
|
1056
|
+
}
|
|
1057
|
+
function enforceBundleCacheLimit() {
|
|
1058
|
+
while (bundleCache.size >= MAX_BUNDLE_CACHE) {
|
|
1059
|
+
let oldestKey = null;
|
|
1060
|
+
let oldestTime = Infinity;
|
|
1061
|
+
for (const [key, entry] of bundleCache) {
|
|
1062
|
+
if (entry.accessTime < oldestTime) {
|
|
1063
|
+
oldestTime = entry.accessTime;
|
|
1064
|
+
oldestKey = key;
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
if (oldestKey) {
|
|
1068
|
+
const evicted = bundleCache.get(oldestKey);
|
|
1069
|
+
bundleCache.delete(oldestKey);
|
|
1070
|
+
console.log(`[CopyIcons] \u{1F5D1}\uFE0F Bundle evicted: ${oldestKey} (${evicted.iconCount} icons, ~${Math.round(evicted.size / 1024)}KB)`);
|
|
1071
|
+
} else {
|
|
1072
|
+
break;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
async function loadIconsFromBundle(prefix, cdnBase, iconNames, elementMap) {
|
|
1077
|
+
const collectionId = getCollectionId(cdnBase);
|
|
1078
|
+
if (!collectionId) return 0;
|
|
1079
|
+
const bundle = await fetchBundleFromCDN(collectionId);
|
|
1080
|
+
if (!bundle) return 0;
|
|
1081
|
+
let loadedCount = 0;
|
|
1082
|
+
for (const name of iconNames) {
|
|
1083
|
+
if (getFromMemory(prefix, name)) continue;
|
|
1084
|
+
const svg = bundle[name];
|
|
1085
|
+
if (!svg || typeof svg !== "string" || !svg.includes("<")) continue;
|
|
1086
|
+
setToMemory(prefix, name, svg);
|
|
1087
|
+
deferCacheWrite(prefix, name, svg);
|
|
1088
|
+
if (elementMap) {
|
|
1089
|
+
const elements = elementMap.get(name);
|
|
1090
|
+
if (elements) {
|
|
1091
|
+
for (const el of elements) {
|
|
1092
|
+
if (isInDOM(el)) injectSVG(el, svg);
|
|
1093
|
+
}
|
|
1094
|
+
loadedCount += elements.length;
|
|
1095
|
+
}
|
|
1096
|
+
} else {
|
|
1097
|
+
try {
|
|
1098
|
+
const iCandidates = document.querySelectorAll(`i.${CSS.escape(prefix)}:not(.svg-loaded)`);
|
|
1099
|
+
for (const el of iCandidates) {
|
|
1100
|
+
const names = getIconNames(el);
|
|
1101
|
+
if (names.includes(name)) {
|
|
1102
|
+
injectSVG(el, svg);
|
|
1103
|
+
loadedCount++;
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
const vCandidates = document.querySelectorAll(`viconic-icon[icon]:not(.svg-loaded)`);
|
|
1107
|
+
for (const el of vCandidates) {
|
|
1108
|
+
const parsed = parseViconicIcon(el.getAttribute("icon"));
|
|
1109
|
+
if (parsed && parsed.prefix === prefix && parsed.iconName === name) {
|
|
1110
|
+
injectSVG(el, svg);
|
|
1111
|
+
loadedCount++;
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
} catch (e) {
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
scheduleBundleFlush(collectionId, prefix);
|
|
1119
|
+
return loadedCount;
|
|
1120
|
+
}
|
|
1121
|
+
function scheduleBundleFlush(collectionId, prefix) {
|
|
1122
|
+
console.log(`[CopyIcons] \u2728 Bundle preserved in memory: ${collectionId} (Fast Pagination Enabled)`);
|
|
1123
|
+
}
|
|
1124
|
+
async function fetchMultiPrefix(bulkPrefixes, map) {
|
|
1125
|
+
const loadId = currentLoadId;
|
|
1126
|
+
const queryParts = [];
|
|
1127
|
+
for (const [prefix, { uncachedNames }] of bulkPrefixes) {
|
|
1128
|
+
queryParts.push(`${prefix}:${uncachedNames.join(",")}`);
|
|
1129
|
+
}
|
|
1130
|
+
const q = queryParts.join("|");
|
|
1131
|
+
try {
|
|
1132
|
+
const url = `${config.batchApiBase}/api/v1/subset/multi-prefix/?q=${encodeURIComponent(q)}`;
|
|
1133
|
+
const response = await Promise.race([
|
|
1134
|
+
fetch(url).catch(() => null),
|
|
1135
|
+
new Promise((resolve) => setTimeout(() => resolve(null), API_TIMEOUT))
|
|
1136
|
+
]);
|
|
1137
|
+
if (response && response.ok) {
|
|
1138
|
+
const data = await response.json();
|
|
1139
|
+
if (loadId !== currentLoadId) return;
|
|
1140
|
+
let totalLoaded = 0;
|
|
1141
|
+
const results = data.results || {};
|
|
1142
|
+
for (const [prefix, icons] of Object.entries(results)) {
|
|
1143
|
+
const group = bulkPrefixes.get(prefix);
|
|
1144
|
+
if (!group) continue;
|
|
1145
|
+
for (const [name, svgData] of Object.entries(icons)) {
|
|
1146
|
+
if (getFromMemory(prefix, name)) continue;
|
|
1147
|
+
const svg = extractSvg(svgData);
|
|
1148
|
+
if (!svg || typeof svg !== "string" || !svg.trim()) continue;
|
|
1149
|
+
setToMemory(prefix, name, svg);
|
|
1150
|
+
deferCacheWrite(prefix, name, svg);
|
|
1151
|
+
const elements = group.elementMap.get(name);
|
|
1152
|
+
if (elements) {
|
|
1153
|
+
for (const el of elements) {
|
|
1154
|
+
if (isInDOM(el)) injectSVG(el, svg);
|
|
1155
|
+
}
|
|
1156
|
+
totalLoaded += elements.length;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
if (totalLoaded > 0) {
|
|
1161
|
+
console.log(`[CopyIcons] \u2705 ${totalLoaded} icons loaded via multi-prefix batch (${bulkPrefixes.size} collections)`);
|
|
1162
|
+
}
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
} catch (e) {
|
|
1166
|
+
}
|
|
1167
|
+
const fallbackPromises = [];
|
|
1168
|
+
for (const [prefix, { uncachedNames, elementMap }] of bulkPrefixes) {
|
|
1169
|
+
fallbackPromises.push(
|
|
1170
|
+
fetchIconsViaAPI(prefix, uncachedNames, elementMap).then((count) => {
|
|
1171
|
+
if (count > 0) console.log(`[CopyIcons] \u2705 ${count} icons loaded for "${prefix}" (API batch)`);
|
|
1172
|
+
})
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
await Promise.allSettled(fallbackPromises);
|
|
1176
|
+
}
|
|
1177
|
+
function extractSvg(iconData) {
|
|
1178
|
+
if (typeof iconData === "string") return iconData;
|
|
1179
|
+
if (typeof iconData === "object" && iconData !== null) return iconData.svg || "";
|
|
1180
|
+
return "";
|
|
1181
|
+
}
|
|
1182
|
+
async function fetchIconsViaAPI(prefix, iconNames, elementMap) {
|
|
1183
|
+
if (failedPrefixes.has(prefix)) return 0;
|
|
1184
|
+
const MAX_BATCH = 100;
|
|
1185
|
+
let totalLoaded = 0;
|
|
1186
|
+
const batches = [];
|
|
1187
|
+
for (let i = 0; i < iconNames.length; i += MAX_BATCH) {
|
|
1188
|
+
batches.push(iconNames.slice(i, i + MAX_BATCH));
|
|
1189
|
+
}
|
|
1190
|
+
const loadId = currentLoadId;
|
|
1191
|
+
const batchPromises = batches.map(async (batch) => {
|
|
1192
|
+
if (loadId !== currentLoadId) return;
|
|
1193
|
+
try {
|
|
1194
|
+
const url = `${config.batchApiBase}/api/v1/subset/by-prefix/?prefix=${encodeURIComponent(prefix)}&icons=${encodeURIComponent(batch.join(","))}`;
|
|
1195
|
+
const response = await Promise.race([
|
|
1196
|
+
fetch(url).catch(() => null),
|
|
1197
|
+
new Promise((resolve) => setTimeout(() => resolve(null), API_TIMEOUT))
|
|
1198
|
+
]);
|
|
1199
|
+
if (!response) return;
|
|
1200
|
+
if (!response.ok) {
|
|
1201
|
+
if (response.status === 404) failedPrefixes.add(prefix);
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
const data = await response.json();
|
|
1205
|
+
if (loadId !== currentLoadId || data.error) return;
|
|
1206
|
+
for (const [name, iconData] of Object.entries(data.icons || {})) {
|
|
1207
|
+
const svg = extractSvg(iconData);
|
|
1208
|
+
if (!svg || typeof svg !== "string" || !svg.trim()) continue;
|
|
1209
|
+
setToMemory(prefix, name, svg);
|
|
1210
|
+
deferCacheWrite(prefix, name, svg);
|
|
1211
|
+
if (elementMap) {
|
|
1212
|
+
const elements = elementMap.get(name);
|
|
1213
|
+
if (elements) {
|
|
1214
|
+
for (const el of elements) {
|
|
1215
|
+
if (isInDOM(el)) injectSVG(el, svg);
|
|
1216
|
+
}
|
|
1217
|
+
totalLoaded += elements.length;
|
|
1218
|
+
}
|
|
1219
|
+
} else {
|
|
1220
|
+
try {
|
|
1221
|
+
const iCandidates = document.querySelectorAll(`i.${CSS.escape(prefix)}:not(.svg-loaded)`);
|
|
1222
|
+
for (const el of iCandidates) {
|
|
1223
|
+
const names = getIconNames(el);
|
|
1224
|
+
if (names.includes(name)) {
|
|
1225
|
+
injectSVG(el, svg);
|
|
1226
|
+
totalLoaded++;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
const vCandidates = document.querySelectorAll(`viconic-icon[icon]:not(.svg-loaded)`);
|
|
1230
|
+
for (const el of vCandidates) {
|
|
1231
|
+
const parsed = parseViconicIcon(el.getAttribute("icon"));
|
|
1232
|
+
if (parsed && parsed.prefix === prefix && parsed.iconName === name) {
|
|
1233
|
+
injectSVG(el, svg);
|
|
1234
|
+
totalLoaded++;
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
} catch (e) {
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
} catch (e) {
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
await Promise.allSettled(batchPromises);
|
|
1245
|
+
return totalLoaded;
|
|
1246
|
+
}
|
|
1247
|
+
async function fetchIconsViaCDN(prefix, cdnBase, iconNames, elementMap) {
|
|
1248
|
+
const loadId = currentLoadId;
|
|
1249
|
+
let loadedCount = 0;
|
|
1250
|
+
let failCount = 0;
|
|
1251
|
+
let notFoundCount = 0;
|
|
1252
|
+
const t0 = performance.now();
|
|
1253
|
+
let index = 0;
|
|
1254
|
+
function nextFetch() {
|
|
1255
|
+
if (index >= iconNames.length || loadId !== currentLoadId) return null;
|
|
1256
|
+
const name = iconNames[index++];
|
|
1257
|
+
const cached = getFromMemory(prefix, name);
|
|
1258
|
+
if (cached) {
|
|
1259
|
+
loadedCount++;
|
|
1260
|
+
if (elementMap) {
|
|
1261
|
+
const elements = elementMap.get(name);
|
|
1262
|
+
if (elements) {
|
|
1263
|
+
for (const el of elements) {
|
|
1264
|
+
if (isInDOM(el)) injectSVG(el, cached);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
return Promise.resolve();
|
|
1269
|
+
}
|
|
1270
|
+
const url = `${cdnBase}/${name}.svg`;
|
|
1271
|
+
return (async () => {
|
|
1272
|
+
const t1 = performance.now();
|
|
1273
|
+
const resp = await Promise.race([
|
|
1274
|
+
fetch(url).catch(() => null),
|
|
1275
|
+
new Promise((resolve) => setTimeout(() => resolve(null), CDN_TIMEOUT))
|
|
1276
|
+
]);
|
|
1277
|
+
if (!resp) {
|
|
1278
|
+
failCount++;
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
if (!resp.ok) {
|
|
1282
|
+
if (resp.status === 404) notFoundCount++;
|
|
1283
|
+
failCount++;
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
const svg = await resp.text().catch(() => "");
|
|
1287
|
+
if (!svg || !svg.includes("<")) return;
|
|
1288
|
+
setToMemory(prefix, name, svg);
|
|
1289
|
+
deferCacheWrite(prefix, name, svg);
|
|
1290
|
+
loadedCount++;
|
|
1291
|
+
if (elementMap) {
|
|
1292
|
+
const elements = elementMap.get(name);
|
|
1293
|
+
if (elements) {
|
|
1294
|
+
for (const el of elements) {
|
|
1295
|
+
if (isInDOM(el)) injectSVG(el, svg);
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
} else {
|
|
1299
|
+
try {
|
|
1300
|
+
const iCandidates = document.querySelectorAll(`i.${CSS.escape(prefix)}:not(.svg-loaded)`);
|
|
1301
|
+
for (const el of iCandidates) {
|
|
1302
|
+
const names = getIconNames(el);
|
|
1303
|
+
if (names.includes(name)) {
|
|
1304
|
+
injectSVG(el, svg);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
const vCandidates = document.querySelectorAll(`viconic-icon[icon]:not(.svg-loaded)`);
|
|
1308
|
+
for (const el of vCandidates) {
|
|
1309
|
+
const parsed = parseViconicIcon(el.getAttribute("icon"));
|
|
1310
|
+
if (parsed && parsed.prefix === prefix && parsed.iconName === name) {
|
|
1311
|
+
injectSVG(el, svg);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
} catch (e) {
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
})();
|
|
1318
|
+
}
|
|
1319
|
+
async function runPool() {
|
|
1320
|
+
const running = /* @__PURE__ */ new Set();
|
|
1321
|
+
for (let i = 0; i < Math.min(CDN_CONCURRENCY, iconNames.length); i++) {
|
|
1322
|
+
const p = nextFetch();
|
|
1323
|
+
if (p) {
|
|
1324
|
+
running.add(p);
|
|
1325
|
+
p.finally(() => running.delete(p));
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
while (running.size > 0) {
|
|
1329
|
+
await Promise.race(running).catch(() => {
|
|
1330
|
+
});
|
|
1331
|
+
const p = nextFetch();
|
|
1332
|
+
if (p) {
|
|
1333
|
+
running.add(p);
|
|
1334
|
+
p.finally(() => running.delete(p));
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
await runPool();
|
|
1339
|
+
const elapsed = Math.round(performance.now() - t0);
|
|
1340
|
+
if (loadedCount > 0 || failCount > 0) {
|
|
1341
|
+
console.log(`[CopyIcons] \u{1F4CA} CDN "${prefix}": ${loadedCount}/${iconNames.length} loaded, ${failCount} failed (${notFoundCount} not-found), ${elapsed}ms`);
|
|
1342
|
+
}
|
|
1343
|
+
return { loadedCount, failCount, notFoundCount };
|
|
1344
|
+
}
|
|
1345
|
+
async function loadSVGsForPrefix(prefix, iconNames, iconList) {
|
|
1346
|
+
if (failedPrefixes.has(prefix)) return;
|
|
1347
|
+
const elementMap = /* @__PURE__ */ new Map();
|
|
1348
|
+
for (const { element, iconName } of iconList) {
|
|
1349
|
+
if (!elementMap.has(iconName)) elementMap.set(iconName, []);
|
|
1350
|
+
elementMap.get(iconName).push(element);
|
|
1351
|
+
}
|
|
1352
|
+
const uncachedNames = [];
|
|
1353
|
+
for (const iconName of iconNames) {
|
|
1354
|
+
const svg = getFromMemory(prefix, iconName) || getPerIconCache(prefix, iconName);
|
|
1355
|
+
if (svg) {
|
|
1356
|
+
const elements = elementMap.get(iconName);
|
|
1357
|
+
if (elements) {
|
|
1358
|
+
for (const el of elements) injectSVG(el, svg);
|
|
1359
|
+
}
|
|
1360
|
+
} else {
|
|
1361
|
+
uncachedNames.push(iconName);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
if (uncachedNames.length === 0) return;
|
|
1365
|
+
if (inflightFetches.has(prefix)) {
|
|
1366
|
+
try {
|
|
1367
|
+
await inflightFetches.get(prefix);
|
|
1368
|
+
} catch (e) {
|
|
1369
|
+
}
|
|
1370
|
+
const stillUncached = [];
|
|
1371
|
+
for (const name of uncachedNames) {
|
|
1372
|
+
const svg = getFromMemory(prefix, name);
|
|
1373
|
+
if (svg) {
|
|
1374
|
+
const elements = elementMap.get(name);
|
|
1375
|
+
if (elements) {
|
|
1376
|
+
for (const el of elements) injectSVG(el, svg);
|
|
1377
|
+
}
|
|
1378
|
+
} else {
|
|
1379
|
+
stillUncached.push(name);
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
if (stillUncached.length === 0) return;
|
|
1383
|
+
uncachedNames.length = 0;
|
|
1384
|
+
uncachedNames.push(...stillUncached);
|
|
1385
|
+
}
|
|
1386
|
+
const map = await loadPrefixMap();
|
|
1387
|
+
const cdnBase = map[prefix];
|
|
1388
|
+
let loadedCount = 0;
|
|
1389
|
+
if (cdnBase) {
|
|
1390
|
+
const colId = getCollectionId(cdnBase);
|
|
1391
|
+
const bundleEntry = bundleCache.get(colId);
|
|
1392
|
+
if (bundleEntry) bundleEntry.accessTime = Date.now();
|
|
1393
|
+
const useBundle = uncachedNames.length >= BUNDLE_THRESHOLD || !!bundleEntry;
|
|
1394
|
+
if (useBundle) {
|
|
1395
|
+
loadedCount = await loadIconsFromBundle(prefix, cdnBase, uncachedNames, elementMap);
|
|
1396
|
+
} else {
|
|
1397
|
+
loadedCount = await fetchIconsViaCDN(prefix, cdnBase, uncachedNames, elementMap);
|
|
1398
|
+
const afterCDN = uncachedNames.filter((n) => !getFromMemory(prefix, n));
|
|
1399
|
+
const BUNDLE_FALLBACK_MIN = 10;
|
|
1400
|
+
if (afterCDN.length >= BUNDLE_FALLBACK_MIN) {
|
|
1401
|
+
console.log(`[CopyIcons] \u{1F504} "${prefix}": ${afterCDN.length}/${uncachedNames.length} CDN failures \u2192 bundle fallback`);
|
|
1402
|
+
try {
|
|
1403
|
+
const bundleLoaded = await loadIconsFromBundle(prefix, cdnBase, afterCDN, elementMap);
|
|
1404
|
+
loadedCount += bundleLoaded;
|
|
1405
|
+
} catch (e) {
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
const remaining = uncachedNames.filter((n) => !getFromMemory(prefix, n));
|
|
1410
|
+
if (remaining.length > 0) {
|
|
1411
|
+
const apiLoaded = await fetchIconsViaAPI(prefix, remaining, elementMap);
|
|
1412
|
+
loadedCount += apiLoaded;
|
|
1413
|
+
}
|
|
1414
|
+
} else {
|
|
1415
|
+
loadedCount = await fetchIconsViaAPI(prefix, uncachedNames, elementMap);
|
|
1416
|
+
}
|
|
1417
|
+
if (loadedCount > 0) {
|
|
1418
|
+
console.log(`[CopyIcons] \u2705 ${loadedCount} icons for "${prefix}" (${cdnBase ? "CDN+fallback" : "API"})`);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
function injectSVGs(iconList, svgData) {
|
|
1422
|
+
const BATCH_SIZE = 100;
|
|
1423
|
+
let index = 0;
|
|
1424
|
+
function processBatch() {
|
|
1425
|
+
const end = Math.min(index + BATCH_SIZE, iconList.length);
|
|
1426
|
+
for (let i = index; i < end; i++) {
|
|
1427
|
+
const { element, iconName } = iconList[i];
|
|
1428
|
+
if (!svgData[iconName]) continue;
|
|
1429
|
+
const iconInfo = svgData[iconName];
|
|
1430
|
+
let svgContent = typeof iconInfo === "object" && iconInfo !== null ? iconInfo.svg || "" : typeof iconInfo === "string" ? iconInfo : "";
|
|
1431
|
+
if (!svgContent || typeof svgContent !== "string") continue;
|
|
1432
|
+
const bgType = element.dataset.bgType;
|
|
1433
|
+
if (bgType && bgType !== "none") {
|
|
1434
|
+
svgContent = addBackground(svgContent, element.dataset);
|
|
1435
|
+
}
|
|
1436
|
+
injectSVG(element, svgContent);
|
|
1437
|
+
}
|
|
1438
|
+
index = end;
|
|
1439
|
+
if (index < iconList.length) requestAnimationFrame(processBatch);
|
|
1440
|
+
}
|
|
1441
|
+
if (iconList.length <= BATCH_SIZE) {
|
|
1442
|
+
processBatch();
|
|
1443
|
+
} else {
|
|
1444
|
+
requestAnimationFrame(processBatch);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
function addBackground(svgContent, dataset) {
|
|
1448
|
+
const bgColor = dataset.bgColor || "#6366f1";
|
|
1449
|
+
const bgScale = parseFloat(dataset.bgScale || "1");
|
|
1450
|
+
const bgType = dataset.bgType;
|
|
1451
|
+
const vbMatch = svgContent.match(/viewBox=["']([^"']+)["']/);
|
|
1452
|
+
if (!vbMatch) return svgContent;
|
|
1453
|
+
const [minX, minY, width, height] = vbMatch[1].split(/[\s,]+/).map(parseFloat);
|
|
1454
|
+
const baseSize = Math.min(width, height);
|
|
1455
|
+
const centerX = minX + width / 2;
|
|
1456
|
+
const centerY = minY + height / 2;
|
|
1457
|
+
const bgSize = Math.min(baseSize * 0.7 * bgScale, baseSize);
|
|
1458
|
+
let bgShape = "";
|
|
1459
|
+
if (bgType === "circle") {
|
|
1460
|
+
bgShape = `<circle cx="${centerX}" cy="${centerY}" r="${bgSize / 2}" fill="${bgColor}" />`;
|
|
1461
|
+
} else if (bgType === "square") {
|
|
1462
|
+
bgShape = `<rect x="${centerX - bgSize / 2}" y="${centerY - bgSize / 2}" width="${bgSize}" height="${bgSize}" fill="${bgColor}" />`;
|
|
1463
|
+
} else if (bgType === "rounded") {
|
|
1464
|
+
const radius = bgSize * 0.15;
|
|
1465
|
+
bgShape = `<rect x="${centerX - bgSize / 2}" y="${centerY - bgSize / 2}" width="${bgSize}" height="${bgSize}" rx="${radius}" ry="${radius}" fill="${bgColor}" />`;
|
|
1466
|
+
}
|
|
1467
|
+
return svgContent.replace(/(<svg[^>]*>)/, `$1${bgShape}`);
|
|
1468
|
+
}
|
|
1469
|
+
function scanDOM(root = document) {
|
|
1470
|
+
const icons = root.querySelectorAll("i[class], i[data-collection]");
|
|
1471
|
+
for (let i = 0; i < icons.length; i++) {
|
|
1472
|
+
registerIcon(icons[i]);
|
|
1473
|
+
}
|
|
1474
|
+
const viconics = root.querySelectorAll("viconic-icon[icon]");
|
|
1475
|
+
for (let i = 0; i < viconics.length; i++) {
|
|
1476
|
+
registerViconicIcon(viconics[i]);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
function setupLazyLoading() {
|
|
1480
|
+
if (!config.lazy || !("IntersectionObserver" in window)) return;
|
|
1481
|
+
intersectionObserver = new IntersectionObserver((entries) => {
|
|
1482
|
+
entries.forEach((entry) => {
|
|
1483
|
+
if (entry.isIntersecting) {
|
|
1484
|
+
registerIcon(entry.target);
|
|
1485
|
+
intersectionObserver.unobserve(entry.target);
|
|
1486
|
+
}
|
|
1487
|
+
});
|
|
1488
|
+
}, {
|
|
1489
|
+
rootMargin: "100px"
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
function registerIconLazy(iconEl) {
|
|
1493
|
+
if (config.lazy && intersectionObserver) {
|
|
1494
|
+
intersectionObserver.observe(iconEl);
|
|
1495
|
+
} else {
|
|
1496
|
+
registerIcon(iconEl);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
let mutationBatchTimer = null;
|
|
1500
|
+
let mutationBatch = [];
|
|
1501
|
+
function setupMutationObserver() {
|
|
1502
|
+
if (mutationObserver) return;
|
|
1503
|
+
mutationObserver = new MutationObserver((mutations) => {
|
|
1504
|
+
for (const m of mutations) {
|
|
1505
|
+
if (!m.addedNodes || m.addedNodes.length === 0) continue;
|
|
1506
|
+
for (const node of m.addedNodes) {
|
|
1507
|
+
if (node.nodeType !== 1) continue;
|
|
1508
|
+
mutationBatch.push(node);
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
if (mutationBatch.length > 500) {
|
|
1512
|
+
mutationBatch.splice(0, mutationBatch.length - 200);
|
|
1513
|
+
}
|
|
1514
|
+
if (mutationBatchTimer) clearTimeout(mutationBatchTimer);
|
|
1515
|
+
if (mutationBatch.length > 20) {
|
|
1516
|
+
mutationBatchTimer = setTimeout(processMutationBatch, 16);
|
|
1517
|
+
} else {
|
|
1518
|
+
mutationBatchTimer = setTimeout(processMutationBatch, 5);
|
|
1519
|
+
}
|
|
1520
|
+
});
|
|
1521
|
+
if (document.body) {
|
|
1522
|
+
mutationObserver.observe(document.body, { childList: true, subtree: true });
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
function processMutationBatch() {
|
|
1526
|
+
if (mutationBatch.length === 0) return;
|
|
1527
|
+
const nodes = mutationBatch.splice(0);
|
|
1528
|
+
for (const node of nodes) {
|
|
1529
|
+
if (!isInDOM(node)) continue;
|
|
1530
|
+
if (node.tagName === "I") {
|
|
1531
|
+
registerIcon(node);
|
|
1532
|
+
} else if (node.tagName === "VICONIC-ICON") {
|
|
1533
|
+
registerViconicIcon(node);
|
|
1534
|
+
} else if (node.querySelectorAll) {
|
|
1535
|
+
const icons = node.querySelectorAll("i[class], i[data-collection]");
|
|
1536
|
+
icons.forEach(registerIcon);
|
|
1537
|
+
const viconics = node.querySelectorAll("viconic-icon[icon]");
|
|
1538
|
+
viconics.forEach(registerViconicIcon);
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
let memoryCheckTimer = null;
|
|
1543
|
+
function getMemoryUsageMB() {
|
|
1544
|
+
if (performance && performance.memory) {
|
|
1545
|
+
return Math.round(performance.memory.usedJSHeapSize / (1024 * 1024));
|
|
1546
|
+
}
|
|
1547
|
+
return null;
|
|
1548
|
+
}
|
|
1549
|
+
function compactMemory(aggressive = false) {
|
|
1550
|
+
const keepCount = aggressive ? 50 : 100;
|
|
1551
|
+
if (memoryCache.size > keepCount) {
|
|
1552
|
+
const keysToDelete = [...memoryCache.keys()].slice(0, memoryCache.size - keepCount);
|
|
1553
|
+
for (const k of keysToDelete) memoryCache.delete(k);
|
|
1554
|
+
}
|
|
1555
|
+
if (bundleCache.size > 0) {
|
|
1556
|
+
const totalIcons = [...bundleCache.values()].reduce((sum, e) => sum + (e.iconCount || 0), 0);
|
|
1557
|
+
bundleCache.clear();
|
|
1558
|
+
console.log(`[CopyIcons] \u{1F5DC}\uFE0F Bundles cleared (${totalIcons} icons freed from memory)`);
|
|
1559
|
+
}
|
|
1560
|
+
if (_deferredWrites.length > 100) {
|
|
1561
|
+
_flushCacheWrites();
|
|
1562
|
+
}
|
|
1563
|
+
const memMB = getMemoryUsageMB();
|
|
1564
|
+
console.log(`[CopyIcons] \u{1F5DC}\uFE0F Memory compacted: cache=${memoryCache.size}, bundles=${bundleCache.size}${memMB ? `, heap=${memMB}MB` : ""}`);
|
|
1565
|
+
}
|
|
1566
|
+
function checkMemoryBudget() {
|
|
1567
|
+
const memMB = getMemoryUsageMB();
|
|
1568
|
+
if (memMB === null) return;
|
|
1569
|
+
if (memMB > MEMORY_BUDGET / (1024 * 1024)) {
|
|
1570
|
+
console.warn(`[CopyIcons] \u26A0\uFE0F Memory ${memMB}MB exceeds budget ${Math.round(MEMORY_BUDGET / (1024 * 1024))}MB \u2014 compacting`);
|
|
1571
|
+
compactMemory(true);
|
|
1572
|
+
} else if (memMB > MEMORY_BUDGET / (1024 * 1024) * 0.85) {
|
|
1573
|
+
compactMemory(false);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
function setupMemoryMonitor() {
|
|
1577
|
+
if (!performance || !performance.memory) return;
|
|
1578
|
+
memoryCheckTimer = setInterval(checkMemoryBudget, MEMORY_CHECK_INTERVAL);
|
|
1579
|
+
}
|
|
1580
|
+
function boot() {
|
|
1581
|
+
console.log("[CopyIcons] \u{1F680} Smart Loader v8.3 (CDN Smart Fallback + Early Abort)");
|
|
1582
|
+
const style = document.createElement("style");
|
|
1583
|
+
style.textContent = `
|
|
1584
|
+
i.svg-loaded {
|
|
1585
|
+
display: inline-flex !important;
|
|
1586
|
+
align-items: center;
|
|
1587
|
+
justify-content: center;
|
|
1588
|
+
background: none !important;
|
|
1589
|
+
background-color: transparent !important;
|
|
1590
|
+
-webkit-mask-image: none !important;
|
|
1591
|
+
mask-image: none !important;
|
|
1592
|
+
}
|
|
1593
|
+
i.svg-loaded::before {
|
|
1594
|
+
content: none !important;
|
|
1595
|
+
display: none !important;
|
|
1596
|
+
}
|
|
1597
|
+
i.svg-loaded:not(.ci-multicolor) svg {
|
|
1598
|
+
width: 1em;
|
|
1599
|
+
height: 1em;
|
|
1600
|
+
fill: currentColor;
|
|
1601
|
+
}
|
|
1602
|
+
i.svg-loaded.ci-multicolor svg {
|
|
1603
|
+
width: 1em;
|
|
1604
|
+
height: 1em;
|
|
1605
|
+
}
|
|
1606
|
+
.modal-icon-display:not(.svg-loaded) {
|
|
1607
|
+
visibility: hidden;
|
|
1608
|
+
}
|
|
1609
|
+
:where(viconic-icon) {
|
|
1610
|
+
display: inline-flex;
|
|
1611
|
+
align-items: center;
|
|
1612
|
+
justify-content: center;
|
|
1613
|
+
vertical-align: middle;
|
|
1614
|
+
font-size: inherit;
|
|
1615
|
+
line-height: 1;
|
|
1616
|
+
width: 1em;
|
|
1617
|
+
height: 1em;
|
|
1618
|
+
opacity: 0;
|
|
1619
|
+
}
|
|
1620
|
+
:where(viconic-icon).svg-loaded {
|
|
1621
|
+
display: inline-flex !important;
|
|
1622
|
+
align-items: center;
|
|
1623
|
+
justify-content: center;
|
|
1624
|
+
background: none !important;
|
|
1625
|
+
background-color: transparent !important;
|
|
1626
|
+
opacity: 1;
|
|
1627
|
+
}
|
|
1628
|
+
:where(viconic-icon).svg-loaded:not(.ci-multicolor) svg {
|
|
1629
|
+
width: 1em;
|
|
1630
|
+
height: 1em;
|
|
1631
|
+
fill: currentColor;
|
|
1632
|
+
}
|
|
1633
|
+
:where(viconic-icon).svg-loaded.ci-multicolor svg {
|
|
1634
|
+
width: 1em;
|
|
1635
|
+
height: 1em;
|
|
1636
|
+
}
|
|
1637
|
+
@keyframes ci-appear { from { opacity: 0; } to { opacity: 1; } }
|
|
1638
|
+
:where(viconic-icon).svg-loaded:not(.cache-hit) { animation: ci-appear 0.12s ease-out; }
|
|
1639
|
+
:where(viconic-icon).cache-hit { opacity: 1 !important; animation: none !important; }
|
|
1640
|
+
i.cache-hit { animation: none !important; }
|
|
1641
|
+
/* --- ANIMATION COMMENTED OUT START ---
|
|
1642
|
+
@keyframes ci-spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
1643
|
+
@keyframes ci-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
|
|
1644
|
+
@keyframes ci-bounce { 0%, 20%, 50%, 80%, 100% { transform: translateY(0); } 40% { transform: translateY(-0.4em); } 60% { transform: translateY(-0.2em); } }
|
|
1645
|
+
@keyframes ci-shake { 0%, 100% { transform: translateX(0); } 10%, 30%, 50%, 70%, 90% { transform: translateX(-0.1em); } 20%, 40%, 60%, 80% { transform: translateX(0.1em); } }
|
|
1646
|
+
@keyframes ci-flip { 0%, 100% { transform: perspective(400px) rotateY(0deg); } 50% { transform: perspective(400px) rotateY(180deg); } }
|
|
1647
|
+
@keyframes ci-heartbeat { 0%, 100% { transform: scale(1); } 14% { transform: scale(1.25); } 28% { transform: scale(1); } 42% { transform: scale(1.25); } 70% { transform: scale(1); } }
|
|
1648
|
+
@keyframes ci-fade { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
|
1649
|
+
@keyframes ci-wobble { 0%, 100% { transform: rotate(0deg); } 15% { transform: rotate(-12deg); } 30% { transform: rotate(8deg); } 45% { transform: rotate(-6deg); } 60% { transform: rotate(4deg); } 75% { transform: rotate(-2deg); } }
|
|
1650
|
+
@keyframes ci-float { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-0.3em); } }
|
|
1651
|
+
@keyframes ci-trace { 0% { fill-opacity: 0; stroke: currentColor; stroke-width: var(--trace-stroke-width, 1px); stroke-dasharray: var(--path-len, 400); stroke-dashoffset: var(--path-len, 400); } 50%, 60% { fill-opacity: 0; stroke: currentColor; stroke-width: var(--trace-stroke-width, 1px); stroke-dasharray: var(--path-len, 400); stroke-dashoffset: 0; } 100% { fill-opacity: 1; stroke: var(--trace-end-stroke, transparent); stroke-width: var(--trace-end-stroke-width, 0); stroke-dasharray: var(--path-len, 400); stroke-dashoffset: 0; } }
|
|
1652
|
+
:where(viconic-icon)[animate="spin"] { animation: ci-spin 1.5s linear infinite !important; }
|
|
1653
|
+
:where(viconic-icon)[animate="pulse"] { animation: ci-pulse 2s ease-in-out infinite !important; }
|
|
1654
|
+
:where(viconic-icon)[animate="bounce"] { animation: ci-bounce 1s ease infinite !important; }
|
|
1655
|
+
:where(viconic-icon)[animate="shake"] { animation: ci-shake 0.8s ease-in-out infinite !important; }
|
|
1656
|
+
:where(viconic-icon)[animate="flip"] { animation: ci-flip 2s ease-in-out infinite !important; }
|
|
1657
|
+
:where(viconic-icon)[animate="heartbeat"] { animation: ci-heartbeat 1.2s ease-in-out infinite !important; }
|
|
1658
|
+
:where(viconic-icon)[animate="fade"] { animation: ci-fade 1.5s ease-in-out infinite !important; }
|
|
1659
|
+
:where(viconic-icon)[animate="wobble"] { animation: ci-wobble 1s ease-in-out infinite !important; }
|
|
1660
|
+
:where(viconic-icon)[animate="float"] { animation: ci-float 2.5s ease-in-out infinite !important; }
|
|
1661
|
+
:where(viconic-icon)[animate="trace"] svg path, :where(viconic-icon)[animate="trace"] svg circle, :where(viconic-icon)[animate="trace"] svg rect, :where(viconic-icon)[animate="trace"] svg polygon, :where(viconic-icon)[animate="trace"] svg polyline { animation: ci-trace 3s ease-in-out infinite !important; }
|
|
1662
|
+
--- ANIMATION COMMENTED OUT END --- */
|
|
1663
|
+
`;
|
|
1664
|
+
document.head.appendChild(style);
|
|
1665
|
+
setupLazyLoading();
|
|
1666
|
+
setupMutationObserver();
|
|
1667
|
+
setupMemoryMonitor();
|
|
1668
|
+
scanDOM(document);
|
|
1669
|
+
}
|
|
1670
|
+
if (document.readyState === "loading") {
|
|
1671
|
+
document.addEventListener("DOMContentLoaded", boot);
|
|
1672
|
+
} else {
|
|
1673
|
+
boot();
|
|
1674
|
+
}
|
|
1675
|
+
window.CopyIcons = {
|
|
1676
|
+
refresh() {
|
|
1677
|
+
scanDOM(document);
|
|
1678
|
+
this.expandAll();
|
|
1679
|
+
},
|
|
1680
|
+
expand() {
|
|
1681
|
+
scanDOM(document);
|
|
1682
|
+
this.expandAll();
|
|
1683
|
+
},
|
|
1684
|
+
expandAll() {
|
|
1685
|
+
},
|
|
1686
|
+
expandElement(element) {
|
|
1687
|
+
if (!element || element.classList.contains("svg-loaded")) return true;
|
|
1688
|
+
processedElements.delete(element);
|
|
1689
|
+
if (element.tagName === "VICONIC-ICON") {
|
|
1690
|
+
registerViconicIcon(element);
|
|
1691
|
+
} else {
|
|
1692
|
+
registerIcon(element);
|
|
1693
|
+
}
|
|
1694
|
+
return true;
|
|
1695
|
+
},
|
|
1696
|
+
reset() {
|
|
1697
|
+
clearPending();
|
|
1698
|
+
},
|
|
1699
|
+
clearPending() {
|
|
1700
|
+
clearPending();
|
|
1701
|
+
},
|
|
1702
|
+
forceProcess(element) {
|
|
1703
|
+
if (element) {
|
|
1704
|
+
element.classList.remove("svg-loaded", "js-processed");
|
|
1705
|
+
element.innerHTML = "";
|
|
1706
|
+
processedElements.delete(element);
|
|
1707
|
+
if (element.tagName === "VICONIC-ICON") {
|
|
1708
|
+
registerViconicIcon(element);
|
|
1709
|
+
} else {
|
|
1710
|
+
registerIcon(element);
|
|
1711
|
+
}
|
|
1712
|
+
} else {
|
|
1713
|
+
document.querySelectorAll("i:not(.svg-loaded)").forEach((icon) => {
|
|
1714
|
+
processedElements.delete(icon);
|
|
1715
|
+
registerIcon(icon);
|
|
1716
|
+
});
|
|
1717
|
+
document.querySelectorAll("viconic-icon[icon]:not(.svg-loaded)").forEach((el) => {
|
|
1718
|
+
processedElements.delete(el);
|
|
1719
|
+
registerViconicIcon(el);
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1722
|
+
},
|
|
1723
|
+
// Viconic utilities
|
|
1724
|
+
camelToKebab,
|
|
1725
|
+
kebabToCamel,
|
|
1726
|
+
classToViconicTag: viconicTagFromClass,
|
|
1727
|
+
parseViconicIcon,
|
|
1728
|
+
iconClassToViconic,
|
|
1729
|
+
injectInstant(element) {
|
|
1730
|
+
if (!element || element.classList.contains("svg-loaded")) return true;
|
|
1731
|
+
let prefix, iconName;
|
|
1732
|
+
if (element.tagName === "VICONIC-ICON") {
|
|
1733
|
+
const parsed = parseViconicIcon(element.getAttribute("icon"));
|
|
1734
|
+
if (!parsed) return false;
|
|
1735
|
+
prefix = parsed.prefix;
|
|
1736
|
+
iconName = parsed.iconName;
|
|
1737
|
+
} else {
|
|
1738
|
+
prefix = detectPrefix(element);
|
|
1739
|
+
if (!prefix) return false;
|
|
1740
|
+
const iconNames = getIconNames(element);
|
|
1741
|
+
if (iconNames.length === 0) return false;
|
|
1742
|
+
iconName = iconNames[0];
|
|
1743
|
+
}
|
|
1744
|
+
const svg = getFromMemory(prefix, iconName) || getPerIconCache(prefix, iconName);
|
|
1745
|
+
if (svg) {
|
|
1746
|
+
injectSVG(element, svg);
|
|
1747
|
+
return true;
|
|
1748
|
+
}
|
|
1749
|
+
processedElements.delete(element);
|
|
1750
|
+
if (element.tagName === "VICONIC-ICON") {
|
|
1751
|
+
registerViconicIcon(element);
|
|
1752
|
+
} else {
|
|
1753
|
+
registerIcon(element);
|
|
1754
|
+
}
|
|
1755
|
+
return false;
|
|
1756
|
+
},
|
|
1757
|
+
preload(prefix, iconNames) {
|
|
1758
|
+
if (!prefix || !iconNames || iconNames.length === 0) return Promise.resolve();
|
|
1759
|
+
const uncached = iconNames.filter(
|
|
1760
|
+
(name) => !getFromMemory(prefix, name) && !getPerIconCache(prefix, name)
|
|
1761
|
+
);
|
|
1762
|
+
if (uncached.length === 0) return Promise.resolve();
|
|
1763
|
+
if (inflightFetches.has(prefix)) {
|
|
1764
|
+
return inflightFetches.get(prefix);
|
|
1765
|
+
}
|
|
1766
|
+
const promise = (async () => {
|
|
1767
|
+
const map = await loadPrefixMap();
|
|
1768
|
+
const cdnBase = map[prefix];
|
|
1769
|
+
if (cdnBase) {
|
|
1770
|
+
const colId = getCollectionId(cdnBase);
|
|
1771
|
+
const useBundle = uncached.length >= BUNDLE_THRESHOLD || colId && bundleCache.has(colId);
|
|
1772
|
+
if (useBundle) {
|
|
1773
|
+
await loadIconsFromBundle(prefix, cdnBase, uncached, null);
|
|
1774
|
+
} else {
|
|
1775
|
+
await fetchIconsViaCDN(prefix, cdnBase, uncached, null).catch(() => {
|
|
1776
|
+
});
|
|
1777
|
+
}
|
|
1778
|
+
const remaining = uncached.filter((n) => !getFromMemory(prefix, n));
|
|
1779
|
+
if (remaining.length === 0) return;
|
|
1780
|
+
await fetchIconsViaAPI(prefix, remaining, null).catch(() => {
|
|
1781
|
+
});
|
|
1782
|
+
} else {
|
|
1783
|
+
await fetchIconsViaAPI(prefix, uncached, null).catch(() => {
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
})().catch(() => {
|
|
1787
|
+
}).finally(() => {
|
|
1788
|
+
inflightFetches.delete(prefix);
|
|
1789
|
+
});
|
|
1790
|
+
inflightFetches.set(prefix, promise);
|
|
1791
|
+
return promise;
|
|
1792
|
+
},
|
|
1793
|
+
// Preload icons from MULTIPLE prefixes in ONE request (for search pages)
|
|
1794
|
+
// Usage: CopyIcons.preloadMulti({ "sol": ["user","home"], "flo": ["arrow"] })
|
|
1795
|
+
preloadMulti(prefixMap2) {
|
|
1796
|
+
if (!prefixMap2 || typeof prefixMap2 !== "object") return Promise.resolve();
|
|
1797
|
+
const toFetch = {};
|
|
1798
|
+
let totalUncached = 0;
|
|
1799
|
+
for (const [prefix, names] of Object.entries(prefixMap2)) {
|
|
1800
|
+
if (!names || !names.length) continue;
|
|
1801
|
+
const uncached = names.filter((n) => !getFromMemory(prefix, n) && !getPerIconCache(prefix, n));
|
|
1802
|
+
if (uncached.length > 0) {
|
|
1803
|
+
toFetch[prefix] = uncached;
|
|
1804
|
+
totalUncached += uncached.length;
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
if (totalUncached === 0) return Promise.resolve();
|
|
1808
|
+
const promise = (async () => {
|
|
1809
|
+
const map = await loadPrefixMap().catch(() => ({}));
|
|
1810
|
+
await Promise.allSettled(
|
|
1811
|
+
Object.entries(toFetch).map(async ([prefix, names]) => {
|
|
1812
|
+
const cdnBase = map[prefix];
|
|
1813
|
+
if (!cdnBase) return;
|
|
1814
|
+
try {
|
|
1815
|
+
await fetchIconsViaCDN(prefix, cdnBase, names, null);
|
|
1816
|
+
const loaded = names.filter((n) => getFromMemory(prefix, n)).length;
|
|
1817
|
+
if (loaded > 0) console.log(`[CopyIcons] \u26A1 CDN preloaded ${loaded} "${prefix}" icons`);
|
|
1818
|
+
} catch (e) {
|
|
1819
|
+
}
|
|
1820
|
+
})
|
|
1821
|
+
);
|
|
1822
|
+
const stillMissing = {};
|
|
1823
|
+
let missingCount = 0;
|
|
1824
|
+
for (const [prefix, names] of Object.entries(toFetch)) {
|
|
1825
|
+
const missed = names.filter((n) => !getFromMemory(prefix, n));
|
|
1826
|
+
if (missed.length > 0) {
|
|
1827
|
+
stillMissing[prefix] = missed;
|
|
1828
|
+
missingCount += missed.length;
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
if (missingCount > 0) {
|
|
1832
|
+
const queryParts = Object.entries(stillMissing).map(([p, ns]) => `${p}:${ns.join(",")}`);
|
|
1833
|
+
const q = queryParts.join("|");
|
|
1834
|
+
try {
|
|
1835
|
+
const url = `${config.batchApiBase}/api/v1/subset/multi-prefix/?q=${encodeURIComponent(q)}`;
|
|
1836
|
+
const resp = await Promise.race([
|
|
1837
|
+
fetch(url).catch(() => null),
|
|
1838
|
+
new Promise((resolve) => setTimeout(() => resolve(null), API_TIMEOUT))
|
|
1839
|
+
]);
|
|
1840
|
+
if (resp && resp.ok) {
|
|
1841
|
+
const data = await resp.json();
|
|
1842
|
+
let count = 0;
|
|
1843
|
+
for (const [prefix, icons] of Object.entries(data.results || {})) {
|
|
1844
|
+
for (const [name, svgData] of Object.entries(icons)) {
|
|
1845
|
+
if (getFromMemory(prefix, name)) continue;
|
|
1846
|
+
const svg = extractSvg(svgData);
|
|
1847
|
+
if (svg && typeof svg === "string" && svg.trim()) {
|
|
1848
|
+
setToMemory(prefix, name, svg);
|
|
1849
|
+
deferCacheWrite(prefix, name, svg);
|
|
1850
|
+
count++;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
if (count > 0) console.log(`[CopyIcons] \u{1F4E5} API fallback preloaded ${count} icons`);
|
|
1855
|
+
}
|
|
1856
|
+
} catch (e) {
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
})().catch(() => {
|
|
1860
|
+
});
|
|
1861
|
+
for (const prefix of Object.keys(toFetch)) {
|
|
1862
|
+
inflightFetches.set(prefix, promise);
|
|
1863
|
+
}
|
|
1864
|
+
promise.finally(() => {
|
|
1865
|
+
for (const prefix of Object.keys(toFetch)) {
|
|
1866
|
+
inflightFetches.delete(prefix);
|
|
1867
|
+
}
|
|
1868
|
+
});
|
|
1869
|
+
return promise;
|
|
1870
|
+
},
|
|
1871
|
+
getStats() {
|
|
1872
|
+
const memMB = getMemoryUsageMB();
|
|
1873
|
+
const bundleStats = [];
|
|
1874
|
+
for (const [id, entry] of bundleCache) {
|
|
1875
|
+
bundleStats.push({ id, icons: entry.iconCount, sizeKB: Math.round(entry.size / 1024) });
|
|
1876
|
+
}
|
|
1877
|
+
return {
|
|
1878
|
+
memoryCache: memoryCache.size,
|
|
1879
|
+
pendingPrefixes: pendingIcons.size,
|
|
1880
|
+
bundleCache: bundleCache.size,
|
|
1881
|
+
bundles: bundleStats,
|
|
1882
|
+
heapMB: memMB,
|
|
1883
|
+
budgetMB: Math.round(MEMORY_BUDGET / (1024 * 1024))
|
|
1884
|
+
};
|
|
1885
|
+
},
|
|
1886
|
+
getConfig() {
|
|
1887
|
+
return { ...config };
|
|
1888
|
+
},
|
|
1889
|
+
clearCache() {
|
|
1890
|
+
memoryCache.clear();
|
|
1891
|
+
const keysToRemove = [];
|
|
1892
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
1893
|
+
const key = localStorage.key(i);
|
|
1894
|
+
if (key && (key.startsWith(ICON_CACHE_PREFIX) || key.startsWith(CACHE_PREFIX))) {
|
|
1895
|
+
keysToRemove.push(key);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
keysToRemove.push(PREFIXMAP_CACHE_KEY);
|
|
1899
|
+
keysToRemove.forEach((k) => localStorage.removeItem(k));
|
|
1900
|
+
prefixMap = null;
|
|
1901
|
+
prefixMapPromise = null;
|
|
1902
|
+
console.log(`[CopyIcons] \u{1F5D1}\uFE0F Cleared ${keysToRemove.length} cache entries`);
|
|
1903
|
+
},
|
|
1904
|
+
compactMemory() {
|
|
1905
|
+
compactMemory(false);
|
|
1906
|
+
}
|
|
1907
|
+
};
|
|
1908
|
+
window.addEventListener("beforeunload", () => {
|
|
1909
|
+
if (_deferredWrites.length > 0) _flushCacheWrites();
|
|
1910
|
+
memoryCache.clear();
|
|
1911
|
+
bundleCache.clear();
|
|
1912
|
+
pendingIcons.clear();
|
|
1913
|
+
if (memoryCheckTimer) {
|
|
1914
|
+
clearInterval(memoryCheckTimer);
|
|
1915
|
+
memoryCheckTimer = null;
|
|
1916
|
+
}
|
|
1917
|
+
if (mutationObserver) {
|
|
1918
|
+
mutationObserver.disconnect();
|
|
1919
|
+
mutationObserver = null;
|
|
1920
|
+
}
|
|
1921
|
+
if (intersectionObserver) {
|
|
1922
|
+
intersectionObserver.disconnect();
|
|
1923
|
+
intersectionObserver = null;
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
if (!window.IconLoader) {
|
|
1927
|
+
window.IconLoader = {
|
|
1928
|
+
expand: () => window.CopyIcons.refresh(),
|
|
1929
|
+
expandElement: (el) => window.CopyIcons.expandElement(el),
|
|
1930
|
+
preloadCollections: () => {
|
|
1931
|
+
}
|
|
1932
|
+
};
|
|
1933
|
+
}
|
|
1934
|
+
})();
|
|
1935
|
+
|
|
1936
|
+
// src/index.jsx
|
|
1937
|
+
var ViconicIcon = ({ name, animate, className, style, ...props }) => {
|
|
1938
|
+
const iconRef = (0, import_react.useRef)(null);
|
|
1939
|
+
(0, import_react.useEffect)(() => {
|
|
1940
|
+
if (window.CopyIcons && iconRef.current) {
|
|
1941
|
+
window.CopyIcons.forceProcess(iconRef.current);
|
|
1942
|
+
}
|
|
1943
|
+
}, [name, animate, className]);
|
|
1944
|
+
return /* @__PURE__ */ import_react.default.createElement(
|
|
1945
|
+
"viconic-icon",
|
|
1946
|
+
{
|
|
1947
|
+
ref: iconRef,
|
|
1948
|
+
icon: name,
|
|
1949
|
+
animate,
|
|
1950
|
+
class: className,
|
|
1951
|
+
style,
|
|
1952
|
+
...props
|
|
1953
|
+
}
|
|
1954
|
+
);
|
|
1955
|
+
};
|
|
1956
|
+
var index_default = ViconicIcon;
|
|
1957
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1958
|
+
0 && (module.exports = {
|
|
1959
|
+
ViconicIcon
|
|
1960
|
+
});
|
|
1961
|
+
/**
|
|
1962
|
+
* CopyIcons Smart Loader v8.3 (CDN Smart Fallback + Early Abort)
|
|
1963
|
+
*
|
|
1964
|
+
* SMART CDN STRATEGY: Uses the fastest path based on how many icons are needed.
|
|
1965
|
+
* - Normal browsing (≤200 per collection): Individual CDN SVGs (parallel, stream-inject)
|
|
1966
|
+
* - Bulk embedding (>200 per collection): Full bundle JSON from CDN (one request)
|
|
1967
|
+
* - Search & Collection pages: Individual CDN (stream-inject, icons appear instantly)
|
|
1968
|
+
* - Kit embedding with many icons: Bundle mode (efficient for 200+ icons)
|
|
1969
|
+
* Django is ONLY used as last-resort fallback.
|
|
1970
|
+
*
|
|
1971
|
+
* Architecture:
|
|
1972
|
+
* 1. Inject cached icons immediately (sync, zero network)
|
|
1973
|
+
* 2. Load prefix-map from CDN/localStorage (maps prefix → CDN base URL)
|
|
1974
|
+
* 3. Smart path selection based on icon count per prefix:
|
|
1975
|
+
* a) Individual CDN: fetch parallel SVGs from CDN (~50ms each, 15 concurrent)
|
|
1976
|
+
* b) Bundle CDN: fetch entire collection JSON (~200-500ms, thousands of icons)
|
|
1977
|
+
* 4. API batch fallback for any icons neither CDN source has
|
|
1978
|
+
*
|
|
1979
|
+
* Simple Usage (zero config):
|
|
1980
|
+
* <script src="https://api.copyicons.click/static/js/copyicons-smart-loader.js"></script>
|
|
1981
|
+
*
|
|
1982
|
+
* @author CopyIcons Team
|
|
1983
|
+
* @license MIT
|
|
1984
|
+
* @version 8.2
|
|
1985
|
+
*/
|