@nice2dev/icons 1.3.14 → 1.3.16
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/CHANGELOG.md +15 -0
- package/dist/cjs/NiceDesktopIconExporter.js +688 -599
- package/dist/cjs/NiceFaviconGenerator.js +603 -417
- package/dist/cjs/NiceIconPicker.js +788 -508
- package/dist/cjs/NiceIconPreview.js +246 -188
- package/dist/cjs/NiceIconSetCreator.js +683 -552
- package/dist/cjs/NiceIconStacker.js +547 -171
- package/dist/cjs/NiceIconValue.js +27 -35
- package/dist/cjs/NtdIconBadge.js +384 -360
- package/dist/cjs/NtdIconMorph.js +419 -354
- package/dist/cjs/NtdStateMorph.js +150 -147
- package/dist/cjs/advancedIconSearch.js +373 -510
- package/dist/cjs/animationControls.js +518 -642
- package/dist/cjs/client.js +12 -15
- package/dist/cjs/createIcon.js +349 -251
- package/dist/cjs/design-tokens.js +324 -390
- package/dist/cjs/figmaExport.js +162 -257
- package/dist/cjs/figmaSync.js +234 -316
- package/dist/cjs/headless-ui.js +240 -368
- package/dist/cjs/iconAnalytics.js +337 -393
- package/dist/cjs/iconHtml.js +64 -86
- package/dist/cjs/iconMetadata.js +392 -472
- package/dist/cjs/iconParts.js +120 -90
- package/dist/cjs/iconStack.js +136 -155
- package/dist/cjs/iconStyles.js +1609 -504
- package/dist/cjs/iconValue.js +359 -430
- package/dist/cjs/icons/accessibility.js +1119 -134
- package/dist/cjs/icons/actions.js +1290 -330
- package/dist/cjs/icons/ai.js +1424 -144
- package/dist/cjs/icons/brand.js +1344 -172
- package/dist/cjs/icons/business.js +903 -193
- package/dist/cjs/icons/devtech.js +1656 -227
- package/dist/cjs/icons/ecommerce.js +802 -111
- package/dist/cjs/icons/education.js +1345 -199
- package/dist/cjs/icons/files.js +1597 -328
- package/dist/cjs/icons/fintech.js +793 -104
- package/dist/cjs/icons/food.js +1507 -153
- package/dist/cjs/icons/fun.js +786 -109
- package/dist/cjs/icons/gaming.js +916 -170
- package/dist/cjs/icons/health.js +623 -112
- package/dist/cjs/icons/index.js +34 -35
- package/dist/cjs/icons/input.js +282 -83
- package/dist/cjs/icons/legal.js +1065 -122
- package/dist/cjs/icons/media.js +1061 -267
- package/dist/cjs/icons/navigation.js +387 -122
- package/dist/cjs/icons/omniverk.js +747 -90
- package/dist/cjs/icons/realestate.js +1520 -193
- package/dist/cjs/icons/saas.js +867 -98
- package/dist/cjs/icons/science.js +1465 -165
- package/dist/cjs/icons/shapes.js +1316 -308
- package/dist/cjs/icons/smarthome.js +786 -90
- package/dist/cjs/icons/social.js +591 -59
- package/dist/cjs/icons/sports.js +1225 -133
- package/dist/cjs/icons/sustainability.js +1211 -139
- package/dist/cjs/icons/travel.js +1470 -148
- package/dist/cjs/icons/ui.js +1508 -315
- package/dist/cjs/icons/weather.js +1160 -173
- package/dist/cjs/index.js +566 -566
- package/dist/cjs/lottieIntegration.js +261 -266
- package/dist/cjs/nicetodev-icons.css +1 -1
- package/dist/cjs/particleEffects.js +270 -251
- package/dist/cjs/resolver.js +229 -284
- package/dist/cjs/rsc.js +175 -193
- package/dist/cjs/sharpen.js +73 -130
- package/dist/cjs/smilAnimations.js +371 -165
- package/dist/cjs/stacking.js +87 -122
- package/dist/cjs/tailwind-plugin.js +280 -320
- package/dist/cjs/types.js +46 -72
- package/dist/cjs/utilities.js +163 -324
- package/dist/cjs/variantProps.js +12 -52
- package/dist/esm/NiceDesktopIconExporter.js +671 -598
- package/dist/esm/NiceFaviconGenerator.js +583 -416
- package/dist/esm/NiceIconPicker.js +789 -507
- package/dist/esm/NiceIconPreview.js +247 -187
- package/dist/esm/NiceIconSetCreator.js +685 -551
- package/dist/esm/NiceIconStacker.js +549 -170
- package/dist/esm/NiceIconValue.js +29 -34
- package/dist/esm/NtdIconBadge.js +388 -359
- package/dist/esm/NtdIconMorph.js +423 -353
- package/dist/esm/NtdStateMorph.js +152 -146
- package/dist/esm/advancedIconSearch.js +391 -509
- package/dist/esm/animationControls.js +527 -641
- package/dist/esm/client.js +48 -10
- package/dist/esm/createIcon.js +350 -250
- package/dist/esm/design-tokens.js +334 -387
- package/dist/esm/figmaExport.js +170 -256
- package/dist/esm/figmaSync.js +237 -315
- package/dist/esm/headless-ui.js +254 -365
- package/dist/esm/iconAnalytics.js +342 -392
- package/dist/esm/iconHtml.js +67 -85
- package/dist/esm/iconMetadata.js +404 -471
- package/dist/esm/iconParts.js +125 -89
- package/dist/esm/iconStack.js +145 -154
- package/dist/esm/iconStyles.js +1620 -503
- package/dist/esm/iconValue.js +361 -429
- package/dist/esm/icons/accessibility.js +1133 -133
- package/dist/esm/icons/actions.js +1320 -329
- package/dist/esm/icons/ai.js +1441 -143
- package/dist/esm/icons/brand.js +1351 -171
- package/dist/esm/icons/business.js +931 -201
- package/dist/esm/icons/devtech.js +1687 -226
- package/dist/esm/icons/ecommerce.js +816 -110
- package/dist/esm/icons/education.js +1364 -198
- package/dist/esm/icons/files.js +1628 -327
- package/dist/esm/icons/fintech.js +807 -103
- package/dist/esm/icons/food.js +1524 -152
- package/dist/esm/icons/fun.js +801 -108
- package/dist/esm/icons/gaming.js +933 -169
- package/dist/esm/icons/health.js +636 -111
- package/dist/esm/icons/index.js +596 -32
- package/dist/esm/icons/input.js +326 -82
- package/dist/esm/icons/legal.js +1079 -121
- package/dist/esm/icons/media.js +1086 -266
- package/dist/esm/icons/navigation.js +401 -121
- package/dist/esm/icons/omniverk.js +759 -89
- package/dist/esm/icons/realestate.js +1544 -198
- package/dist/esm/icons/saas.js +880 -97
- package/dist/esm/icons/science.js +1482 -164
- package/dist/esm/icons/shapes.js +1343 -307
- package/dist/esm/icons/smarthome.js +799 -89
- package/dist/esm/icons/social.js +599 -58
- package/dist/esm/icons/sports.js +1240 -132
- package/dist/esm/icons/sustainability.js +1226 -138
- package/dist/esm/icons/travel.js +1486 -147
- package/dist/esm/icons/ui.js +1540 -314
- package/dist/esm/icons/weather.js +1181 -172
- package/dist/esm/index.js +756 -58
- package/dist/esm/lottieIntegration.js +264 -265
- package/dist/esm/nicetodev-icons.css +1 -1
- package/dist/esm/particleEffects.js +271 -250
- package/dist/esm/resolver.js +231 -283
- package/dist/esm/rsc.js +182 -192
- package/dist/esm/sharpen.js +78 -129
- package/dist/esm/smilAnimations.js +376 -164
- package/dist/esm/stacking.js +99 -121
- package/dist/esm/tailwind-plugin.js +282 -319
- package/dist/esm/types.js +50 -71
- package/dist/esm/utilities.js +177 -321
- package/dist/esm/variantProps.js +23 -52
- package/dist/types/icons/index.d.ts +1 -1
- package/dist/types/icons/science.d.ts +9 -0
- package/package.json +15 -22
|
@@ -1,428 +1,378 @@
|
|
|
1
|
-
import { useState, useCallback, useEffect } from
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @file iconAnalytics.ts
|
|
5
|
-
* @module @nice2dev/icons
|
|
6
|
-
* @description PRO-28.3 — Icon Usage Analytics Service
|
|
7
|
-
*
|
|
8
|
-
* Tracks and analyzes icon usage patterns:
|
|
9
|
-
* - Most used icons
|
|
10
|
-
* - Icons per project
|
|
11
|
-
* - Missing icons suggestions
|
|
12
|
-
* - Unused icons cleanup
|
|
13
|
-
*/
|
|
14
|
-
/* ══════════════════════════════════════════════════════════════════════════════
|
|
15
|
-
Analytics Service Class
|
|
16
|
-
══════════════════════════════════════════════════════════════════════════════ */
|
|
1
|
+
import { useState, useCallback, useEffect } from "react";
|
|
17
2
|
const DEFAULT_CONFIG = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
3
|
+
maxEventsInMemory: 1e4,
|
|
4
|
+
persistInterval: 6e4,
|
|
5
|
+
// 1 minute
|
|
6
|
+
storageKey: "nice2dev-icon-analytics",
|
|
7
|
+
debug: false
|
|
22
8
|
};
|
|
23
|
-
/**
|
|
24
|
-
* Icon Analytics Service for tracking usage patterns.
|
|
25
|
-
*/
|
|
26
9
|
class IconAnalyticsService {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
10
|
+
constructor(config = {}) {
|
|
11
|
+
this.events = [];
|
|
12
|
+
this.iconStats = /* @__PURE__ */ new Map();
|
|
13
|
+
this.persistTimer = null;
|
|
14
|
+
this.knownIcons = /* @__PURE__ */ new Set();
|
|
15
|
+
this.deprecatedIcons = /* @__PURE__ */ new Set();
|
|
16
|
+
this.searchMisses = /* @__PURE__ */ new Map();
|
|
17
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
18
|
+
this.loadFromStorage();
|
|
19
|
+
this.startPersistTimer();
|
|
20
|
+
}
|
|
21
|
+
/* ────────────────────────────────────────────────────────────────────────────
|
|
22
|
+
Event Tracking
|
|
23
|
+
──────────────────────────────────────────────────────────────────────────── */
|
|
24
|
+
/**
|
|
25
|
+
* Track an icon usage event.
|
|
26
|
+
*/
|
|
27
|
+
track(event) {
|
|
28
|
+
const fullEvent = {
|
|
29
|
+
...event,
|
|
30
|
+
timestamp: Date.now()
|
|
31
|
+
};
|
|
32
|
+
this.events.push(fullEvent);
|
|
33
|
+
this.updateStats(fullEvent);
|
|
34
|
+
if (this.events.length > this.config.maxEventsInMemory) {
|
|
35
|
+
this.events = this.events.slice(-this.config.maxEventsInMemory);
|
|
37
36
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
──────────────────────────────────────────────────────────────────────────── */
|
|
41
|
-
/**
|
|
42
|
-
* Track an icon usage event.
|
|
43
|
-
*/
|
|
44
|
-
track(event) {
|
|
45
|
-
const fullEvent = {
|
|
46
|
-
...event,
|
|
47
|
-
timestamp: Date.now(),
|
|
48
|
-
};
|
|
49
|
-
this.events.push(fullEvent);
|
|
50
|
-
this.updateStats(fullEvent);
|
|
51
|
-
// Trim events if over limit
|
|
52
|
-
if (this.events.length > this.config.maxEventsInMemory) {
|
|
53
|
-
this.events = this.events.slice(-this.config.maxEventsInMemory);
|
|
54
|
-
}
|
|
55
|
-
if (this.config.debug) {
|
|
56
|
-
console.log('[IconAnalytics] Tracked:', event.iconName);
|
|
57
|
-
}
|
|
37
|
+
if (this.config.debug) {
|
|
38
|
+
console.log("[IconAnalytics] Tracked:", event.iconName);
|
|
58
39
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const count = this.searchMisses.get(normalized) || 0;
|
|
68
|
-
this.searchMisses.set(normalized, count + 1);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Track a search miss (icon not found).
|
|
43
|
+
*/
|
|
44
|
+
trackSearchMiss(query) {
|
|
45
|
+
const normalized = query.toLowerCase().trim();
|
|
46
|
+
if (normalized.length < 2) {
|
|
47
|
+
return;
|
|
69
48
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
49
|
+
const count = this.searchMisses.get(normalized) || 0;
|
|
50
|
+
this.searchMisses.set(normalized, count + 1);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Register an icon as known (for finding unused).
|
|
54
|
+
*/
|
|
55
|
+
registerKnownIcon(name, deprecated = false) {
|
|
56
|
+
this.knownIcons.add(name);
|
|
57
|
+
if (deprecated) {
|
|
58
|
+
this.deprecatedIcons.add(name);
|
|
78
59
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Bulk register icons.
|
|
63
|
+
*/
|
|
64
|
+
registerKnownIcons(icons) {
|
|
65
|
+
for (const icon of icons) {
|
|
66
|
+
this.registerKnownIcon(icon.name, icon.deprecated);
|
|
86
67
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
stats.totalUsage++;
|
|
106
|
-
stats.lastUsed = event.timestamp;
|
|
107
|
-
if (event.context) {
|
|
108
|
-
const ctxCount = stats.contexts.get(event.context) || 0;
|
|
109
|
-
stats.contexts.set(event.context, ctxCount + 1);
|
|
110
|
-
}
|
|
111
|
-
if (event.projectId) {
|
|
112
|
-
stats.projects.add(event.projectId);
|
|
113
|
-
}
|
|
114
|
-
if (event.variant) {
|
|
115
|
-
const varCount = stats.variants.get(event.variant) || 0;
|
|
116
|
-
stats.variants.set(event.variant, varCount + 1);
|
|
117
|
-
}
|
|
118
|
-
if (event.size !== undefined) {
|
|
119
|
-
const sizeNum = typeof event.size === 'number' ? event.size : parseInt(event.size, 10);
|
|
120
|
-
if (!isNaN(sizeNum)) {
|
|
121
|
-
// Running average
|
|
122
|
-
stats.avgSize = (stats.avgSize * (stats.totalUsage - 1) + sizeNum) / stats.totalUsage;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
/* ────────────────────────────────────────────────────────────────────────────
|
|
127
|
-
Queries & Reports
|
|
128
|
-
──────────────────────────────────────────────────────────────────────────── */
|
|
129
|
-
/**
|
|
130
|
-
* Get usage stats for a specific icon.
|
|
131
|
-
*/
|
|
132
|
-
getIconStats(iconName) {
|
|
133
|
-
return this.iconStats.get(iconName);
|
|
68
|
+
}
|
|
69
|
+
/* ────────────────────────────────────────────────────────────────────────────
|
|
70
|
+
Stats Management
|
|
71
|
+
──────────────────────────────────────────────────────────────────────────── */
|
|
72
|
+
updateStats(event) {
|
|
73
|
+
let stats = this.iconStats.get(event.iconName);
|
|
74
|
+
if (!stats) {
|
|
75
|
+
stats = {
|
|
76
|
+
name: event.iconName,
|
|
77
|
+
totalUsage: 0,
|
|
78
|
+
firstUsed: event.timestamp,
|
|
79
|
+
lastUsed: event.timestamp,
|
|
80
|
+
contexts: /* @__PURE__ */ new Map(),
|
|
81
|
+
projects: /* @__PURE__ */ new Set(),
|
|
82
|
+
variants: /* @__PURE__ */ new Map(),
|
|
83
|
+
avgSize: 0
|
|
84
|
+
};
|
|
85
|
+
this.iconStats.set(event.iconName, stats);
|
|
134
86
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
.sort((a, b) => b.totalUsage - a.totalUsage)
|
|
141
|
-
.slice(0, limit)
|
|
142
|
-
.map((s) => ({ name: s.name, count: s.totalUsage }));
|
|
87
|
+
stats.totalUsage++;
|
|
88
|
+
stats.lastUsed = event.timestamp;
|
|
89
|
+
if (event.context) {
|
|
90
|
+
const ctxCount = stats.contexts.get(event.context) || 0;
|
|
91
|
+
stats.contexts.set(event.context, ctxCount + 1);
|
|
143
92
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
*/
|
|
147
|
-
getLeastUsedIcons(limit = 20) {
|
|
148
|
-
return Array.from(this.iconStats.values())
|
|
149
|
-
.sort((a, b) => a.totalUsage - b.totalUsage)
|
|
150
|
-
.slice(0, limit)
|
|
151
|
-
.map((s) => ({ name: s.name, count: s.totalUsage }));
|
|
93
|
+
if (event.projectId) {
|
|
94
|
+
stats.projects.add(event.projectId);
|
|
152
95
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
getUnusedIcons() {
|
|
157
|
-
const usedIcons = new Set(this.iconStats.keys());
|
|
158
|
-
return Array.from(this.knownIcons).filter((name) => !usedIcons.has(name));
|
|
96
|
+
if (event.variant) {
|
|
97
|
+
const varCount = stats.variants.get(event.variant) || 0;
|
|
98
|
+
stats.variants.set(event.variant, varCount + 1);
|
|
159
99
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
for (const name of this.deprecatedIcons) {
|
|
166
|
-
const stats = this.iconStats.get(name);
|
|
167
|
-
if (stats) {
|
|
168
|
-
results.push({ name, count: stats.totalUsage });
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
return results.sort((a, b) => b.count - a.count);
|
|
100
|
+
if (event.size !== void 0) {
|
|
101
|
+
const sizeNum = typeof event.size === "number" ? event.size : parseInt(event.size, 10);
|
|
102
|
+
if (!isNaN(sizeNum)) {
|
|
103
|
+
stats.avgSize = (stats.avgSize * (stats.totalUsage - 1) + sizeNum) / stats.totalUsage;
|
|
104
|
+
}
|
|
172
105
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
106
|
+
}
|
|
107
|
+
/* ────────────────────────────────────────────────────────────────────────────
|
|
108
|
+
Queries & Reports
|
|
109
|
+
──────────────────────────────────────────────────────────────────────────── */
|
|
110
|
+
/**
|
|
111
|
+
* Get usage stats for a specific icon.
|
|
112
|
+
*/
|
|
113
|
+
getIconStats(iconName) {
|
|
114
|
+
return this.iconStats.get(iconName);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Get most used icons.
|
|
118
|
+
*/
|
|
119
|
+
getMostUsedIcons(limit = 20) {
|
|
120
|
+
return Array.from(this.iconStats.values()).sort((a, b) => b.totalUsage - a.totalUsage).slice(0, limit).map((s) => ({ name: s.name, count: s.totalUsage }));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Get least used icons (icons used but rarely).
|
|
124
|
+
*/
|
|
125
|
+
getLeastUsedIcons(limit = 20) {
|
|
126
|
+
return Array.from(this.iconStats.values()).sort((a, b) => a.totalUsage - b.totalUsage).slice(0, limit).map((s) => ({ name: s.name, count: s.totalUsage }));
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Get icons that have never been used.
|
|
130
|
+
*/
|
|
131
|
+
getUnusedIcons() {
|
|
132
|
+
const usedIcons = new Set(this.iconStats.keys());
|
|
133
|
+
return Array.from(this.knownIcons).filter((name) => !usedIcons.has(name));
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Get deprecated icons that are still being used.
|
|
137
|
+
*/
|
|
138
|
+
getDeprecatedInUse() {
|
|
139
|
+
const results = [];
|
|
140
|
+
for (const name of this.deprecatedIcons) {
|
|
141
|
+
const stats = this.iconStats.get(name);
|
|
142
|
+
if (stats) {
|
|
143
|
+
results.push({ name, count: stats.totalUsage });
|
|
144
|
+
}
|
|
181
145
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
146
|
+
return results.sort((a, b) => b.count - a.count);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Get potentially missing icons based on search misses.
|
|
150
|
+
*/
|
|
151
|
+
getPotentiallyMissingIcons(minMisses = 3) {
|
|
152
|
+
return Array.from(this.searchMisses.entries()).filter(([_, count]) => count >= minMisses).sort((a, b) => b[1] - a[1]).map(([query, misses]) => ({ query, misses }));
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Get icons used in a specific project.
|
|
156
|
+
*/
|
|
157
|
+
getIconsByProject(projectId) {
|
|
158
|
+
const projectIcons = [];
|
|
159
|
+
for (const stats of this.iconStats.values()) {
|
|
160
|
+
if (stats.projects.has(projectId)) {
|
|
161
|
+
const count = this.events.filter(
|
|
162
|
+
(e) => e.iconName === stats.name && e.projectId === projectId
|
|
163
|
+
).length;
|
|
164
|
+
projectIcons.push({ name: stats.name, count });
|
|
165
|
+
}
|
|
195
166
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
}
|
|
214
|
-
summary.totalUsage++;
|
|
215
|
-
}
|
|
216
|
-
// Calculate unique icons and top icons per project
|
|
217
|
-
for (const summary of projects.values()) {
|
|
218
|
-
const iconsInProject = this.getIconsByProject(summary.projectId);
|
|
219
|
-
summary.uniqueIcons = iconsInProject.length;
|
|
220
|
-
summary.topIcons = iconsInProject.slice(0, 10);
|
|
221
|
-
}
|
|
222
|
-
return Array.from(projects.values());
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Generate full analytics report.
|
|
226
|
-
*/
|
|
227
|
-
generateReport(periodDays) {
|
|
228
|
-
var _a;
|
|
229
|
-
const now = Date.now();
|
|
230
|
-
const periodStart = periodDays
|
|
231
|
-
? now - periodDays * 24 * 60 * 60 * 1000
|
|
232
|
-
: ((_a = this.events[0]) === null || _a === void 0 ? void 0 : _a.timestamp) || now;
|
|
233
|
-
const periodEnd = now;
|
|
234
|
-
// Filter events for period
|
|
235
|
-
const periodEvents = periodDays
|
|
236
|
-
? this.events.filter((e) => e.timestamp >= periodStart)
|
|
237
|
-
: this.events;
|
|
238
|
-
// Unique icons
|
|
239
|
-
const uniqueIcons = new Set(periodEvents.map((e) => e.iconName)).size;
|
|
240
|
-
// Most used
|
|
241
|
-
const mostUsed = this.getMostUsedIcons(20);
|
|
242
|
-
// Trending (icons with increasing usage recently)
|
|
243
|
-
const recentCutoff = now - 7 * 24 * 60 * 60 * 1000; // last 7 days
|
|
244
|
-
const recentEvents = this.events.filter((e) => e.timestamp >= recentCutoff);
|
|
245
|
-
const olderEvents = this.events.filter((e) => e.timestamp < recentCutoff && e.timestamp >= recentCutoff - 7 * 24 * 60 * 60 * 1000);
|
|
246
|
-
const recentCounts = new Map();
|
|
247
|
-
const olderCounts = new Map();
|
|
248
|
-
for (const e of recentEvents) {
|
|
249
|
-
recentCounts.set(e.iconName, (recentCounts.get(e.iconName) || 0) + 1);
|
|
250
|
-
}
|
|
251
|
-
for (const e of olderEvents) {
|
|
252
|
-
olderCounts.set(e.iconName, (olderCounts.get(e.iconName) || 0) + 1);
|
|
253
|
-
}
|
|
254
|
-
const trendingIcons = [];
|
|
255
|
-
for (const [name, recent] of recentCounts) {
|
|
256
|
-
const older = olderCounts.get(name) || 1;
|
|
257
|
-
const growth = ((recent - older) / older) * 100;
|
|
258
|
-
if (growth > 50) {
|
|
259
|
-
trendingIcons.push({ name, growth });
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
trendingIcons.sort((a, b) => b.growth - a.growth);
|
|
263
|
-
// Underutilized
|
|
264
|
-
const underutilized = this.getLeastUsedIcons(50)
|
|
265
|
-
.filter((i) => i.count === 1)
|
|
266
|
-
.map((i) => i.name);
|
|
267
|
-
// Potentially missing
|
|
268
|
-
const potentiallyMissing = this.getPotentiallyMissingIcons()
|
|
269
|
-
.slice(0, 20)
|
|
270
|
-
.map((p) => p.query);
|
|
271
|
-
// Deprecated in use
|
|
272
|
-
const deprecatedInUse = this.getDeprecatedInUse();
|
|
273
|
-
// Project summaries
|
|
274
|
-
const projectSummaries = this.getProjectSummaries();
|
|
275
|
-
return {
|
|
276
|
-
periodStart,
|
|
277
|
-
periodEnd,
|
|
278
|
-
totalEvents: periodEvents.length,
|
|
279
|
-
uniqueIcons,
|
|
280
|
-
mostUsed,
|
|
281
|
-
trendingIcons: trendingIcons.slice(0, 10),
|
|
282
|
-
underutilized: underutilized.slice(0, 20),
|
|
283
|
-
potentiallyMissing,
|
|
284
|
-
deprecatedInUse,
|
|
285
|
-
projectSummaries,
|
|
167
|
+
return projectIcons.sort((a, b) => b.count - a.count);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Get project summaries.
|
|
171
|
+
*/
|
|
172
|
+
getProjectSummaries() {
|
|
173
|
+
const projects = /* @__PURE__ */ new Map();
|
|
174
|
+
for (const event of this.events) {
|
|
175
|
+
const pid = event.projectId || "unknown";
|
|
176
|
+
let summary = projects.get(pid);
|
|
177
|
+
if (!summary) {
|
|
178
|
+
summary = {
|
|
179
|
+
projectId: pid,
|
|
180
|
+
uniqueIcons: 0,
|
|
181
|
+
totalUsage: 0,
|
|
182
|
+
topIcons: [],
|
|
183
|
+
categories: /* @__PURE__ */ new Map()
|
|
286
184
|
};
|
|
185
|
+
projects.set(pid, summary);
|
|
186
|
+
}
|
|
187
|
+
summary.totalUsage++;
|
|
287
188
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
if (this.config.persistInterval > 0 && typeof window !== 'undefined') {
|
|
293
|
-
this.persistTimer = setInterval(() => {
|
|
294
|
-
this.saveToStorage();
|
|
295
|
-
}, this.config.persistInterval);
|
|
296
|
-
}
|
|
189
|
+
for (const summary of projects.values()) {
|
|
190
|
+
const iconsInProject = this.getIconsByProject(summary.projectId);
|
|
191
|
+
summary.uniqueIcons = iconsInProject.length;
|
|
192
|
+
summary.topIcons = iconsInProject.slice(0, 10);
|
|
297
193
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
194
|
+
return Array.from(projects.values());
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Generate full analytics report.
|
|
198
|
+
*/
|
|
199
|
+
generateReport(periodDays) {
|
|
200
|
+
const now = Date.now();
|
|
201
|
+
const periodStart = periodDays ? now - periodDays * 24 * 60 * 60 * 1e3 : this.events[0]?.timestamp || now;
|
|
202
|
+
const periodEnd = now;
|
|
203
|
+
const periodEvents = periodDays ? this.events.filter((e) => e.timestamp >= periodStart) : this.events;
|
|
204
|
+
const uniqueIcons = new Set(periodEvents.map((e) => e.iconName)).size;
|
|
205
|
+
const mostUsed = this.getMostUsedIcons(20);
|
|
206
|
+
const recentCutoff = now - 7 * 24 * 60 * 60 * 1e3;
|
|
207
|
+
const recentEvents = this.events.filter((e) => e.timestamp >= recentCutoff);
|
|
208
|
+
const olderEvents = this.events.filter(
|
|
209
|
+
(e) => e.timestamp < recentCutoff && e.timestamp >= recentCutoff - 7 * 24 * 60 * 60 * 1e3
|
|
210
|
+
);
|
|
211
|
+
const recentCounts = /* @__PURE__ */ new Map();
|
|
212
|
+
const olderCounts = /* @__PURE__ */ new Map();
|
|
213
|
+
for (const e of recentEvents) {
|
|
214
|
+
recentCounts.set(e.iconName, (recentCounts.get(e.iconName) || 0) + 1);
|
|
303
215
|
}
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return;
|
|
307
|
-
}
|
|
308
|
-
try {
|
|
309
|
-
const data = {
|
|
310
|
-
events: this.events.slice(-this.config.maxEventsInMemory),
|
|
311
|
-
searchMisses: Array.from(this.searchMisses.entries()),
|
|
312
|
-
savedAt: Date.now(),
|
|
313
|
-
};
|
|
314
|
-
localStorage.setItem(this.config.storageKey, JSON.stringify(data));
|
|
315
|
-
if (this.config.debug) {
|
|
316
|
-
console.log('[IconAnalytics] Saved to storage');
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
catch (err) {
|
|
320
|
-
console.warn('[IconAnalytics] Failed to save:', err);
|
|
321
|
-
}
|
|
216
|
+
for (const e of olderEvents) {
|
|
217
|
+
olderCounts.set(e.iconName, (olderCounts.get(e.iconName) || 0) + 1);
|
|
322
218
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const data = JSON.parse(stored);
|
|
331
|
-
this.events = data.events || [];
|
|
332
|
-
this.searchMisses = new Map(data.searchMisses || []);
|
|
333
|
-
// Rebuild stats from events
|
|
334
|
-
for (const event of this.events) {
|
|
335
|
-
this.updateStats(event);
|
|
336
|
-
}
|
|
337
|
-
if (this.config.debug) {
|
|
338
|
-
console.log('[IconAnalytics] Loaded from storage:', this.events.length, 'events');
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
catch (err) {
|
|
343
|
-
console.warn('[IconAnalytics] Failed to load:', err);
|
|
344
|
-
}
|
|
219
|
+
const trendingIcons = [];
|
|
220
|
+
for (const [name, recent] of recentCounts) {
|
|
221
|
+
const older = olderCounts.get(name) || 1;
|
|
222
|
+
const growth = (recent - older) / older * 100;
|
|
223
|
+
if (growth > 50) {
|
|
224
|
+
trendingIcons.push({ name, growth });
|
|
225
|
+
}
|
|
345
226
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
227
|
+
trendingIcons.sort((a, b) => b.growth - a.growth);
|
|
228
|
+
const underutilized = this.getLeastUsedIcons(50).filter((i) => i.count === 1).map((i) => i.name);
|
|
229
|
+
const potentiallyMissing = this.getPotentiallyMissingIcons().slice(0, 20).map((p) => p.query);
|
|
230
|
+
const deprecatedInUse = this.getDeprecatedInUse();
|
|
231
|
+
const projectSummaries = this.getProjectSummaries();
|
|
232
|
+
return {
|
|
233
|
+
periodStart,
|
|
234
|
+
periodEnd,
|
|
235
|
+
totalEvents: periodEvents.length,
|
|
236
|
+
uniqueIcons,
|
|
237
|
+
mostUsed,
|
|
238
|
+
trendingIcons: trendingIcons.slice(0, 10),
|
|
239
|
+
underutilized: underutilized.slice(0, 20),
|
|
240
|
+
potentiallyMissing,
|
|
241
|
+
deprecatedInUse,
|
|
242
|
+
projectSummaries
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
/* ────────────────────────────────────────────────────────────────────────────
|
|
246
|
+
Persistence
|
|
247
|
+
──────────────────────────────────────────────────────────────────────────── */
|
|
248
|
+
startPersistTimer() {
|
|
249
|
+
if (this.config.persistInterval > 0 && typeof window !== "undefined") {
|
|
250
|
+
this.persistTimer = setInterval(() => {
|
|
350
251
|
this.saveToStorage();
|
|
252
|
+
}, this.config.persistInterval);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
stopPersistTimer() {
|
|
256
|
+
if (this.persistTimer) {
|
|
257
|
+
clearInterval(this.persistTimer);
|
|
258
|
+
this.persistTimer = null;
|
|
351
259
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
260
|
+
}
|
|
261
|
+
saveToStorage() {
|
|
262
|
+
if (typeof localStorage === "undefined") {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const data = {
|
|
267
|
+
events: this.events.slice(-this.config.maxEventsInMemory),
|
|
268
|
+
searchMisses: Array.from(this.searchMisses.entries()),
|
|
269
|
+
savedAt: Date.now()
|
|
270
|
+
};
|
|
271
|
+
localStorage.setItem(this.config.storageKey, JSON.stringify(data));
|
|
272
|
+
if (this.config.debug) {
|
|
273
|
+
console.log("[IconAnalytics] Saved to storage");
|
|
274
|
+
}
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.warn("[IconAnalytics] Failed to save:", err);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
loadFromStorage() {
|
|
280
|
+
if (typeof localStorage === "undefined") {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const stored = localStorage.getItem(this.config.storageKey);
|
|
285
|
+
if (stored) {
|
|
286
|
+
const data = JSON.parse(stored);
|
|
287
|
+
this.events = data.events || [];
|
|
288
|
+
this.searchMisses = new Map(data.searchMisses || []);
|
|
289
|
+
for (const event of this.events) {
|
|
290
|
+
this.updateStats(event);
|
|
291
|
+
}
|
|
292
|
+
if (this.config.debug) {
|
|
293
|
+
console.log("[IconAnalytics] Loaded from storage:", this.events.length, "events");
|
|
361
294
|
}
|
|
295
|
+
}
|
|
296
|
+
} catch (err) {
|
|
297
|
+
console.warn("[IconAnalytics] Failed to load:", err);
|
|
362
298
|
}
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Force save to storage.
|
|
302
|
+
*/
|
|
303
|
+
persist() {
|
|
304
|
+
this.saveToStorage();
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Clear all analytics data.
|
|
308
|
+
*/
|
|
309
|
+
clear() {
|
|
310
|
+
this.events = [];
|
|
311
|
+
this.iconStats.clear();
|
|
312
|
+
this.searchMisses.clear();
|
|
313
|
+
if (typeof localStorage !== "undefined") {
|
|
314
|
+
localStorage.removeItem(this.config.storageKey);
|
|
369
315
|
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Destroy the service (cleanup timers).
|
|
319
|
+
*/
|
|
320
|
+
destroy() {
|
|
321
|
+
this.stopPersistTimer();
|
|
322
|
+
this.saveToStorage();
|
|
323
|
+
}
|
|
370
324
|
}
|
|
371
|
-
/* ══════════════════════════════════════════════════════════════════════════════
|
|
372
|
-
Singleton Instance
|
|
373
|
-
══════════════════════════════════════════════════════════════════════════════ */
|
|
374
325
|
let defaultInstance = null;
|
|
375
|
-
/**
|
|
376
|
-
* Get the default analytics service instance.
|
|
377
|
-
*/
|
|
378
326
|
function getIconAnalytics(config) {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
327
|
+
if (!defaultInstance) {
|
|
328
|
+
defaultInstance = new IconAnalyticsService(config);
|
|
329
|
+
}
|
|
330
|
+
return defaultInstance;
|
|
383
331
|
}
|
|
384
|
-
/**
|
|
385
|
-
* Track an icon usage (shorthand).
|
|
386
|
-
*/
|
|
387
332
|
function trackIconUsage(iconName, options = {}) {
|
|
388
|
-
|
|
333
|
+
getIconAnalytics().track({ iconName, ...options });
|
|
389
334
|
}
|
|
390
|
-
/**
|
|
391
|
-
* Track a search miss (shorthand).
|
|
392
|
-
*/
|
|
393
335
|
function trackIconSearchMiss(query) {
|
|
394
|
-
|
|
336
|
+
getIconAnalytics().trackSearchMiss(query);
|
|
395
337
|
}
|
|
396
|
-
/**
|
|
397
|
-
* React hook for icon analytics.
|
|
398
|
-
*/
|
|
399
338
|
function useIconAnalytics(projectId) {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
339
|
+
const analytics = getIconAnalytics();
|
|
340
|
+
const [mostUsed, setMostUsed] = useState([]);
|
|
341
|
+
const [unused, setUnused] = useState([]);
|
|
342
|
+
const [report, setReport] = useState(null);
|
|
343
|
+
const refresh = useCallback(() => {
|
|
344
|
+
setMostUsed(analytics.getMostUsedIcons(20));
|
|
345
|
+
setUnused(analytics.getUnusedIcons().slice(0, 50));
|
|
346
|
+
}, [analytics]);
|
|
347
|
+
const track = useCallback(
|
|
348
|
+
(iconName, context) => {
|
|
349
|
+
analytics.track({ iconName, context, projectId });
|
|
350
|
+
refresh();
|
|
351
|
+
},
|
|
352
|
+
[analytics, projectId, refresh]
|
|
353
|
+
);
|
|
354
|
+
const generateReportFn = useCallback(
|
|
355
|
+
(periodDays) => {
|
|
356
|
+
setReport(analytics.generateReport(periodDays));
|
|
357
|
+
},
|
|
358
|
+
[analytics]
|
|
359
|
+
);
|
|
360
|
+
useEffect(() => {
|
|
361
|
+
refresh();
|
|
362
|
+
}, [refresh]);
|
|
363
|
+
return {
|
|
364
|
+
track,
|
|
365
|
+
mostUsed,
|
|
366
|
+
unused,
|
|
367
|
+
refresh,
|
|
368
|
+
report,
|
|
369
|
+
generateReport: generateReportFn
|
|
370
|
+
};
|
|
426
371
|
}
|
|
427
|
-
|
|
428
|
-
|
|
372
|
+
export {
|
|
373
|
+
IconAnalyticsService,
|
|
374
|
+
getIconAnalytics,
|
|
375
|
+
trackIconSearchMiss,
|
|
376
|
+
trackIconUsage,
|
|
377
|
+
useIconAnalytics
|
|
378
|
+
};
|