@nice2dev/icons 1.0.5 → 1.0.8

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.
Files changed (57) hide show
  1. package/dist/cjs/NiceDesktopIconExporter.js +648 -0
  2. package/dist/cjs/NiceDesktopIconExporter.js.map +1 -0
  3. package/dist/cjs/NiceFaviconGenerator.js +429 -0
  4. package/dist/cjs/NiceFaviconGenerator.js.map +1 -0
  5. package/dist/cjs/NiceIconEditor.js +443 -0
  6. package/dist/cjs/NiceIconEditor.js.map +1 -0
  7. package/dist/cjs/NiceIconPreview.js +198 -0
  8. package/dist/cjs/NiceIconPreview.js.map +1 -0
  9. package/dist/cjs/NiceIconSetCreator.js +565 -0
  10. package/dist/cjs/NiceIconSetCreator.js.map +1 -0
  11. package/dist/cjs/_virtual/_commonjsHelpers.js +8 -0
  12. package/dist/cjs/_virtual/_commonjsHelpers.js.map +1 -0
  13. package/dist/cjs/_virtual/client.js +33 -0
  14. package/dist/cjs/_virtual/client.js.map +1 -0
  15. package/dist/cjs/_virtual/client2.js +6 -0
  16. package/dist/cjs/_virtual/client2.js.map +1 -0
  17. package/dist/cjs/advancedIconSearch.js +548 -0
  18. package/dist/cjs/advancedIconSearch.js.map +1 -0
  19. package/dist/cjs/iconAnalytics.js +395 -152
  20. package/dist/cjs/iconAnalytics.js.map +1 -1
  21. package/dist/cjs/index.js +42 -4
  22. package/dist/cjs/index.js.map +1 -1
  23. package/dist/cjs/node_modules/react-dom/client.js +39 -0
  24. package/dist/cjs/node_modules/react-dom/client.js.map +1 -0
  25. package/dist/esm/NiceDesktopIconExporter.js +642 -0
  26. package/dist/esm/NiceDesktopIconExporter.js.map +1 -0
  27. package/dist/esm/NiceFaviconGenerator.js +426 -0
  28. package/dist/esm/NiceFaviconGenerator.js.map +1 -0
  29. package/dist/esm/NiceIconEditor.js +440 -0
  30. package/dist/esm/NiceIconEditor.js.map +1 -0
  31. package/dist/esm/NiceIconPreview.js +196 -0
  32. package/dist/esm/NiceIconPreview.js.map +1 -0
  33. package/dist/esm/NiceIconSetCreator.js +562 -0
  34. package/dist/esm/NiceIconSetCreator.js.map +1 -0
  35. package/dist/esm/_virtual/_commonjsHelpers.js +6 -0
  36. package/dist/esm/_virtual/_commonjsHelpers.js.map +1 -0
  37. package/dist/esm/_virtual/client.js +28 -0
  38. package/dist/esm/_virtual/client.js.map +1 -0
  39. package/dist/esm/_virtual/client2.js +4 -0
  40. package/dist/esm/_virtual/client2.js.map +1 -0
  41. package/dist/esm/advancedIconSearch.js +529 -0
  42. package/dist/esm/advancedIconSearch.js.map +1 -0
  43. package/dist/esm/iconAnalytics.js +393 -152
  44. package/dist/esm/iconAnalytics.js.map +1 -1
  45. package/dist/esm/index.js +7 -1
  46. package/dist/esm/index.js.map +1 -1
  47. package/dist/esm/node_modules/react-dom/client.js +37 -0
  48. package/dist/esm/node_modules/react-dom/client.js.map +1 -0
  49. package/dist/types/NiceDesktopIconExporter.d.ts +119 -0
  50. package/dist/types/NiceFaviconGenerator.d.ts +64 -0
  51. package/dist/types/NiceIconEditor.d.ts +97 -0
  52. package/dist/types/NiceIconPreview.d.ts +47 -0
  53. package/dist/types/NiceIconSetCreator.d.ts +97 -0
  54. package/dist/types/advancedIconSearch.d.ts +218 -0
  55. package/dist/types/iconAnalytics.d.ts +219 -112
  56. package/dist/types/index.d.ts +18 -6
  57. package/package.json +2 -2
@@ -0,0 +1,642 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { useState, useMemo, useCallback } from 'react';
3
+
4
+ /* ══════════════════════════════════════════════════════════════════════════════
5
+ Constants
6
+ ══════════════════════════════════════════════════════════════════════════════ */
7
+ /** Standard desktop icon sizes used across platforms */
8
+ const DESKTOP_ICON_SIZES = [
9
+ { size: 16, label: '16×16', platforms: ['windows', 'linux'] },
10
+ { size: 24, label: '24×24', platforms: ['linux'] },
11
+ { size: 32, label: '32×32', platforms: ['windows', 'linux'] },
12
+ { size: 48, label: '48×48', platforms: ['windows', 'linux'] },
13
+ { size: 64, label: '64×64', platforms: ['windows', 'linux'] },
14
+ { size: 128, label: '128×128', platforms: ['windows', 'macos', 'linux'] },
15
+ { size: 256, label: '256×256', platforms: ['windows', 'macos', 'linux'] },
16
+ { size: 512, label: '512×512', platforms: ['macos', 'linux'] },
17
+ { size: 1024, label: '1024×1024', platforms: ['macos'] },
18
+ ];
19
+ /** Retina/HiDPI scale factors */
20
+ const RETINA_VARIANTS = [
21
+ { scale: 1, suffix: '' },
22
+ { scale: 2, suffix: '@2x' },
23
+ { scale: 3, suffix: '@3x' },
24
+ ];
25
+ /** Default export configuration */
26
+ const DEFAULT_CONFIG = {
27
+ name: 'icon',
28
+ includeWindows: true,
29
+ includeMacOS: true,
30
+ includeLinux: true,
31
+ includeRetina: true,
32
+ backgroundColor: undefined, // transparent
33
+ padding: 0.1,
34
+ fillColor: '#1e1e1e',
35
+ sizes: [16, 32, 48, 64, 128, 256, 512],
36
+ };
37
+ /** Linux hicolor theme icon directories */
38
+ const LINUX_HICOLOR_DIRS = [
39
+ { size: 16, dir: 'hicolor/16x16/apps' },
40
+ { size: 24, dir: 'hicolor/24x24/apps' },
41
+ { size: 32, dir: 'hicolor/32x32/apps' },
42
+ { size: 48, dir: 'hicolor/48x48/apps' },
43
+ { size: 64, dir: 'hicolor/64x64/apps' },
44
+ { size: 128, dir: 'hicolor/128x128/apps' },
45
+ { size: 256, dir: 'hicolor/256x256/apps' },
46
+ { size: 512, dir: 'hicolor/512x512/apps' },
47
+ ];
48
+ /* ══════════════════════════════════════════════════════════════════════════════
49
+ Utility Functions
50
+ ══════════════════════════════════════════════════════════════════════════════ */
51
+ /**
52
+ * Render an icon component to a canvas at the specified size
53
+ */
54
+ async function renderIconToCanvas(IconComponent, size, fillColor, backgroundColor, padding = 0.1) {
55
+ const canvas = document.createElement('canvas');
56
+ canvas.width = size;
57
+ canvas.height = size;
58
+ const ctx = canvas.getContext('2d');
59
+ // Fill background (or make transparent)
60
+ if (backgroundColor) {
61
+ ctx.fillStyle = backgroundColor;
62
+ ctx.fillRect(0, 0, size, size);
63
+ }
64
+ else {
65
+ ctx.clearRect(0, 0, size, size);
66
+ }
67
+ // Render icon to temporary container
68
+ const container = document.createElement('div');
69
+ const iconSize = Math.round(size * (1 - padding * 2));
70
+ const offset = Math.round(size * padding);
71
+ const { createRoot } = await import('./_virtual/client.js').then(function (n) { return n.c; });
72
+ const root = createRoot(container);
73
+ return new Promise((resolve) => {
74
+ root.render(jsx(IconComponent, { size: iconSize, fill: fillColor }));
75
+ // Wait for render
76
+ setTimeout(() => {
77
+ const svg = container.querySelector('svg');
78
+ if (svg) {
79
+ const svgData = new XMLSerializer().serializeToString(svg);
80
+ const img = new Image();
81
+ img.onload = () => {
82
+ ctx.drawImage(img, offset, offset, iconSize, iconSize);
83
+ root.unmount();
84
+ resolve(canvas);
85
+ };
86
+ img.onerror = () => {
87
+ root.unmount();
88
+ resolve(canvas);
89
+ };
90
+ img.src = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgData)));
91
+ }
92
+ else {
93
+ root.unmount();
94
+ resolve(canvas);
95
+ }
96
+ }, 50);
97
+ });
98
+ }
99
+ /**
100
+ * Convert canvas to PNG blob
101
+ */
102
+ function canvasToBlob(canvas) {
103
+ return new Promise((resolve, reject) => {
104
+ canvas.toBlob((blob) => {
105
+ if (blob)
106
+ resolve(blob);
107
+ else
108
+ reject(new Error('Failed to convert canvas to blob'));
109
+ }, 'image/png');
110
+ });
111
+ }
112
+ /**
113
+ * Create a Windows ICO file from multiple PNG images.
114
+ * ICO format: https://en.wikipedia.org/wiki/ICO_(file_format)
115
+ */
116
+ async function createIcoFile(pngBuffers) {
117
+ // Sort by size descending
118
+ const sorted = [...pngBuffers].sort((a, b) => b.size - a.size);
119
+ // ICO header: 6 bytes
120
+ // Image entry: 16 bytes each
121
+ const headerSize = 6;
122
+ const entrySize = 16;
123
+ const directorySize = headerSize + entrySize * sorted.length;
124
+ // Calculate total size and offsets
125
+ let currentOffset = directorySize;
126
+ const entries = [];
127
+ for (const png of sorted) {
128
+ entries.push({ size: png.size, offset: currentOffset, data: png.data });
129
+ currentOffset += png.data.byteLength;
130
+ }
131
+ // Create the ICO buffer
132
+ const totalSize = currentOffset;
133
+ const buffer = new ArrayBuffer(totalSize);
134
+ const view = new DataView(buffer);
135
+ const uint8 = new Uint8Array(buffer);
136
+ // Write ICO header
137
+ view.setUint16(0, 0, true); // Reserved (must be 0)
138
+ view.setUint16(2, 1, true); // Type (1 = ICO)
139
+ view.setUint16(4, sorted.length, true); // Number of images
140
+ // Write image directory entries
141
+ let entryOffset = headerSize;
142
+ for (const entry of entries) {
143
+ const size = entry.size > 255 ? 0 : entry.size; // 0 means 256
144
+ view.setUint8(entryOffset + 0, size); // Width
145
+ view.setUint8(entryOffset + 1, size); // Height
146
+ view.setUint8(entryOffset + 2, 0); // Color palette (0 = no palette)
147
+ view.setUint8(entryOffset + 3, 0); // Reserved
148
+ view.setUint16(entryOffset + 4, 1, true); // Color planes
149
+ view.setUint16(entryOffset + 6, 32, true); // Bits per pixel
150
+ view.setUint32(entryOffset + 8, entry.data.byteLength, true); // Image data size
151
+ view.setUint32(entryOffset + 12, entry.offset, true); // Image data offset
152
+ entryOffset += entrySize;
153
+ }
154
+ // Write image data
155
+ for (const entry of entries) {
156
+ uint8.set(new Uint8Array(entry.data), entry.offset);
157
+ }
158
+ return new Blob([buffer], { type: 'image/x-icon' });
159
+ }
160
+ /**
161
+ * Create a ZIP file containing multiple files
162
+ */
163
+ async function createZipFile(files) {
164
+ // Simple ZIP implementation without external libraries
165
+ // Uses store (no compression) method for simplicity
166
+ const encoder = new TextEncoder();
167
+ const fileEntries = [];
168
+ let offset = 0;
169
+ // Prepare file entries
170
+ for (const file of files) {
171
+ const nameBytes = encoder.encode(file.name);
172
+ let dataBytes;
173
+ if (typeof file.data === 'string') {
174
+ dataBytes = encoder.encode(file.data);
175
+ }
176
+ else if (file.data instanceof Blob) {
177
+ dataBytes = new Uint8Array(await file.data.arrayBuffer());
178
+ }
179
+ else {
180
+ dataBytes = new Uint8Array(file.data);
181
+ }
182
+ const crc32 = calculateCRC32(dataBytes);
183
+ fileEntries.push({ name: nameBytes, data: dataBytes, crc32, offset });
184
+ // Local file header size: 30 + name length + data length
185
+ offset += 30 + nameBytes.length + dataBytes.length;
186
+ }
187
+ const centralDirectoryOffset = offset;
188
+ // Calculate central directory size
189
+ let centralDirSize = 0;
190
+ for (const entry of fileEntries) {
191
+ centralDirSize += 46 + entry.name.length;
192
+ }
193
+ // Total size: local headers + data + central directory + end of central directory
194
+ const totalSize = offset + centralDirSize + 22;
195
+ const buffer = new ArrayBuffer(totalSize);
196
+ const view = new DataView(buffer);
197
+ const bytes = new Uint8Array(buffer);
198
+ let pos = 0;
199
+ // Write local file headers and data
200
+ for (const entry of fileEntries) {
201
+ // Local file header signature
202
+ view.setUint32(pos, 0x04034b50, true);
203
+ pos += 4;
204
+ // Version needed
205
+ view.setUint16(pos, 20, true);
206
+ pos += 2;
207
+ // General purpose bit flag
208
+ view.setUint16(pos, 0, true);
209
+ pos += 2;
210
+ // Compression method (0 = store)
211
+ view.setUint16(pos, 0, true);
212
+ pos += 2;
213
+ // File time & date (use current)
214
+ const now = new Date();
215
+ const dosTime = ((now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() >> 1)) & 0xffff;
216
+ const dosDate = (((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate()) & 0xffff;
217
+ view.setUint16(pos, dosTime, true);
218
+ pos += 2;
219
+ view.setUint16(pos, dosDate, true);
220
+ pos += 2;
221
+ // CRC-32
222
+ view.setUint32(pos, entry.crc32, true);
223
+ pos += 4;
224
+ // Compressed size
225
+ view.setUint32(pos, entry.data.length, true);
226
+ pos += 4;
227
+ // Uncompressed size
228
+ view.setUint32(pos, entry.data.length, true);
229
+ pos += 4;
230
+ // File name length
231
+ view.setUint16(pos, entry.name.length, true);
232
+ pos += 2;
233
+ // Extra field length
234
+ view.setUint16(pos, 0, true);
235
+ pos += 2;
236
+ // File name
237
+ bytes.set(entry.name, pos);
238
+ pos += entry.name.length;
239
+ // File data
240
+ bytes.set(entry.data, pos);
241
+ pos += entry.data.length;
242
+ }
243
+ // Write central directory
244
+ for (const entry of fileEntries) {
245
+ // Central directory file header signature
246
+ view.setUint32(pos, 0x02014b50, true);
247
+ pos += 4;
248
+ // Version made by
249
+ view.setUint16(pos, 20, true);
250
+ pos += 2;
251
+ // Version needed
252
+ view.setUint16(pos, 20, true);
253
+ pos += 2;
254
+ // General purpose bit flag
255
+ view.setUint16(pos, 0, true);
256
+ pos += 2;
257
+ // Compression method
258
+ view.setUint16(pos, 0, true);
259
+ pos += 2;
260
+ // File time & date
261
+ const now = new Date();
262
+ const dosTime = ((now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() >> 1)) & 0xffff;
263
+ const dosDate = (((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate()) & 0xffff;
264
+ view.setUint16(pos, dosTime, true);
265
+ pos += 2;
266
+ view.setUint16(pos, dosDate, true);
267
+ pos += 2;
268
+ // CRC-32
269
+ view.setUint32(pos, entry.crc32, true);
270
+ pos += 4;
271
+ // Compressed size
272
+ view.setUint32(pos, entry.data.length, true);
273
+ pos += 4;
274
+ // Uncompressed size
275
+ view.setUint32(pos, entry.data.length, true);
276
+ pos += 4;
277
+ // File name length
278
+ view.setUint16(pos, entry.name.length, true);
279
+ pos += 2;
280
+ // Extra field length
281
+ view.setUint16(pos, 0, true);
282
+ pos += 2;
283
+ // File comment length
284
+ view.setUint16(pos, 0, true);
285
+ pos += 2;
286
+ // Disk number start
287
+ view.setUint16(pos, 0, true);
288
+ pos += 2;
289
+ // Internal file attributes
290
+ view.setUint16(pos, 0, true);
291
+ pos += 2;
292
+ // External file attributes
293
+ view.setUint32(pos, 0, true);
294
+ pos += 4;
295
+ // Relative offset of local header
296
+ view.setUint32(pos, entry.offset, true);
297
+ pos += 4;
298
+ // File name
299
+ bytes.set(entry.name, pos);
300
+ pos += entry.name.length;
301
+ }
302
+ // End of central directory record
303
+ view.setUint32(pos, 0x06054b50, true);
304
+ pos += 4;
305
+ // Disk number
306
+ view.setUint16(pos, 0, true);
307
+ pos += 2;
308
+ // Disk number with CD
309
+ view.setUint16(pos, 0, true);
310
+ pos += 2;
311
+ // Number of entries on this disk
312
+ view.setUint16(pos, fileEntries.length, true);
313
+ pos += 2;
314
+ // Total number of entries
315
+ view.setUint16(pos, fileEntries.length, true);
316
+ pos += 2;
317
+ // Size of central directory
318
+ view.setUint32(pos, centralDirSize, true);
319
+ pos += 4;
320
+ // Offset of central directory
321
+ view.setUint32(pos, centralDirectoryOffset, true);
322
+ pos += 4;
323
+ // Comment length
324
+ view.setUint16(pos, 0, true);
325
+ return new Blob([buffer], { type: 'application/zip' });
326
+ }
327
+ /**
328
+ * Calculate CRC-32 for ZIP files
329
+ */
330
+ function calculateCRC32(data) {
331
+ let crc = 0xffffffff;
332
+ const table = getCRC32Table();
333
+ for (let i = 0; i < data.length; i++) {
334
+ crc = (crc >>> 8) ^ table[(crc ^ data[i]) & 0xff];
335
+ }
336
+ return (crc ^ 0xffffffff) >>> 0;
337
+ }
338
+ let crc32Table = null;
339
+ function getCRC32Table() {
340
+ if (crc32Table)
341
+ return crc32Table;
342
+ crc32Table = new Uint32Array(256);
343
+ for (let i = 0; i < 256; i++) {
344
+ let c = i;
345
+ for (let j = 0; j < 8; j++) {
346
+ c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
347
+ }
348
+ crc32Table[i] = c;
349
+ }
350
+ return crc32Table;
351
+ }
352
+ /* ══════════════════════════════════════════════════════════════════════════════
353
+ Hook
354
+ ══════════════════════════════════════════════════════════════════════════════ */
355
+ /**
356
+ * Hook for exporting desktop icons in multiple formats and sizes.
357
+ *
358
+ * @example
359
+ * ```tsx
360
+ * const { exportAll, downloadAll, icons, isExporting } = useDesktopIconExporter(MyIcon);
361
+ *
362
+ * // Export all icons
363
+ * await exportAll();
364
+ *
365
+ * // Download as ZIP
366
+ * await downloadAll();
367
+ * ```
368
+ */
369
+ function useDesktopIconExporter(icon, initialConfig) {
370
+ const [config, setConfig] = useState({
371
+ ...DEFAULT_CONFIG,
372
+ ...initialConfig,
373
+ });
374
+ const [icons, setIcons] = useState([]);
375
+ const [isExporting, setIsExporting] = useState(false);
376
+ const [progress, setProgress] = useState(0);
377
+ // Calculate total number of icons to export
378
+ const totalIcons = useMemo(() => {
379
+ let count = 0;
380
+ const scales = config.includeRetina ? RETINA_VARIANTS.length : 1;
381
+ for (const size of config.sizes) {
382
+ if (config.includeWindows || config.includeLinux)
383
+ count += scales;
384
+ if (config.includeMacOS && size >= 128)
385
+ count += scales;
386
+ }
387
+ return count;
388
+ }, [config]);
389
+ // Export all icons as PNGs
390
+ const exportAll = useCallback(async () => {
391
+ setIsExporting(true);
392
+ setProgress(0);
393
+ const exported = [];
394
+ let completed = 0;
395
+ try {
396
+ const scales = config.includeRetina ? RETINA_VARIANTS : [RETINA_VARIANTS[0]];
397
+ for (const size of config.sizes) {
398
+ for (const variant of scales) {
399
+ const actualSize = size * variant.scale;
400
+ const canvas = await renderIconToCanvas(icon, actualSize, config.fillColor, config.backgroundColor, config.padding);
401
+ const dataUrl = canvas.toDataURL('image/png');
402
+ const blob = await canvasToBlob(canvas);
403
+ // Determine platforms
404
+ const platforms = [];
405
+ if (config.includeWindows && size <= 256)
406
+ platforms.push('windows');
407
+ if (config.includeMacOS && size >= 128)
408
+ platforms.push('macos');
409
+ if (config.includeLinux)
410
+ platforms.push('linux');
411
+ exported.push({
412
+ filename: `${config.name}-${size}x${size}${variant.suffix}.png`,
413
+ size,
414
+ scale: variant.scale,
415
+ platform: platforms.join(','),
416
+ dataUrl,
417
+ blob,
418
+ });
419
+ completed++;
420
+ setProgress((completed / totalIcons) * 100);
421
+ }
422
+ }
423
+ setIcons(exported);
424
+ }
425
+ catch (error) {
426
+ console.error('Failed to export icons:', error);
427
+ }
428
+ finally {
429
+ setIsExporting(false);
430
+ }
431
+ }, [icon, config, totalIcons]);
432
+ // Export Windows ICO file
433
+ const exportWindows = useCallback(async () => {
434
+ const windowsSizes = config.sizes.filter((s) => s <= 256);
435
+ const pngBuffers = [];
436
+ for (const size of windowsSizes) {
437
+ const canvas = await renderIconToCanvas(icon, size, config.fillColor, config.backgroundColor, config.padding);
438
+ const blob = await canvasToBlob(canvas);
439
+ const buffer = await blob.arrayBuffer();
440
+ pngBuffers.push({ size, data: buffer });
441
+ }
442
+ const icoBlob = await createIcoFile(pngBuffers);
443
+ return URL.createObjectURL(icoBlob);
444
+ }, [icon, config]);
445
+ // Export macOS icons (as ZIP with PNG set - ICNS requires native tools)
446
+ const exportMacOS = useCallback(async () => {
447
+ const macSizes = config.sizes.filter((s) => s >= 128);
448
+ const files = [];
449
+ for (const size of macSizes) {
450
+ const scales = config.includeRetina ? RETINA_VARIANTS : [RETINA_VARIANTS[0]];
451
+ for (const variant of scales) {
452
+ const actualSize = size * variant.scale;
453
+ const canvas = await renderIconToCanvas(icon, actualSize, config.fillColor, config.backgroundColor, config.padding);
454
+ const blob = await canvasToBlob(canvas);
455
+ // macOS naming convention: icon_128x128@2x.png
456
+ files.push({
457
+ name: `${config.name}.iconset/icon_${size}x${size}${variant.suffix}.png`,
458
+ data: blob,
459
+ });
460
+ }
461
+ }
462
+ // Add README with iconutil instructions
463
+ files.push({
464
+ name: 'README.txt',
465
+ data: new Blob([
466
+ `macOS Icon Set
467
+ =============
468
+
469
+ To convert to .icns format, use the following command in Terminal:
470
+
471
+ iconutil -c icns ${config.name}.iconset
472
+
473
+ This will create ${config.name}.icns
474
+
475
+ Note: iconutil is only available on macOS.
476
+ `,
477
+ ], { type: 'text/plain' }),
478
+ });
479
+ const zipBlob = await createZipFile(files);
480
+ return URL.createObjectURL(zipBlob);
481
+ }, [icon, config]);
482
+ // Export Linux PNG set
483
+ const exportLinux = useCallback(async () => {
484
+ const linuxIcons = [];
485
+ for (const { size, dir } of LINUX_HICOLOR_DIRS) {
486
+ if (!config.sizes.includes(size))
487
+ continue;
488
+ const canvas = await renderIconToCanvas(icon, size, config.fillColor, config.backgroundColor, config.padding);
489
+ const dataUrl = canvas.toDataURL('image/png');
490
+ const blob = await canvasToBlob(canvas);
491
+ linuxIcons.push({
492
+ filename: `${dir}/${config.name}.png`,
493
+ size,
494
+ scale: 1,
495
+ platform: 'linux',
496
+ dataUrl,
497
+ blob,
498
+ });
499
+ }
500
+ return linuxIcons;
501
+ }, [icon, config]);
502
+ // Download single icon
503
+ const downloadSingle = useCallback((exportedIcon) => {
504
+ const link = document.createElement('a');
505
+ link.href = exportedIcon.dataUrl;
506
+ link.download = exportedIcon.filename;
507
+ link.click();
508
+ }, []);
509
+ // Download all as ZIP
510
+ const downloadAll = useCallback(async () => {
511
+ setIsExporting(true);
512
+ setProgress(0);
513
+ try {
514
+ const files = [];
515
+ // Export all PNG icons first
516
+ await exportAll();
517
+ // Add PNG icons
518
+ for (const exportedIcon of icons) {
519
+ if (exportedIcon.blob) {
520
+ files.push({ name: `png/${exportedIcon.filename}`, data: exportedIcon.blob });
521
+ }
522
+ }
523
+ // Add Windows ICO
524
+ if (config.includeWindows) {
525
+ const windowsSizes = config.sizes.filter((s) => s <= 256);
526
+ const pngBuffers = [];
527
+ for (const size of windowsSizes) {
528
+ const canvas = await renderIconToCanvas(icon, size, config.fillColor, config.backgroundColor, config.padding);
529
+ const blob = await canvasToBlob(canvas);
530
+ pngBuffers.push({ size, data: await blob.arrayBuffer() });
531
+ }
532
+ const icoBlob = await createIcoFile(pngBuffers);
533
+ files.push({ name: `windows/${config.name}.ico`, data: icoBlob });
534
+ }
535
+ // Add macOS iconset
536
+ if (config.includeMacOS) {
537
+ const macSizes = config.sizes.filter((s) => s >= 128);
538
+ const scales = config.includeRetina ? RETINA_VARIANTS : [RETINA_VARIANTS[0]];
539
+ for (const size of macSizes) {
540
+ for (const variant of scales) {
541
+ const actualSize = size * variant.scale;
542
+ const canvas = await renderIconToCanvas(icon, actualSize, config.fillColor, config.backgroundColor, config.padding);
543
+ const blob = await canvasToBlob(canvas);
544
+ files.push({
545
+ name: `macos/${config.name}.iconset/icon_${size}x${size}${variant.suffix}.png`,
546
+ data: blob,
547
+ });
548
+ }
549
+ }
550
+ files.push({
551
+ name: 'macos/README.txt',
552
+ data: `To convert to .icns: iconutil -c icns ${config.name}.iconset`,
553
+ });
554
+ }
555
+ // Add Linux icons
556
+ if (config.includeLinux) {
557
+ for (const { size, dir } of LINUX_HICOLOR_DIRS) {
558
+ if (!config.sizes.includes(size))
559
+ continue;
560
+ const canvas = await renderIconToCanvas(icon, size, config.fillColor, config.backgroundColor, config.padding);
561
+ const blob = await canvasToBlob(canvas);
562
+ files.push({ name: `linux/${dir}/${config.name}.png`, data: blob });
563
+ }
564
+ }
565
+ // Create and download ZIP
566
+ const zipBlob = await createZipFile(files);
567
+ const url = URL.createObjectURL(zipBlob);
568
+ const link = document.createElement('a');
569
+ link.href = url;
570
+ link.download = `${config.name}-icons.zip`;
571
+ link.click();
572
+ URL.revokeObjectURL(url);
573
+ }
574
+ catch (error) {
575
+ console.error('Failed to download icons:', error);
576
+ }
577
+ finally {
578
+ setIsExporting(false);
579
+ }
580
+ }, [icon, icons, config, exportAll]);
581
+ return {
582
+ config,
583
+ setConfig,
584
+ icons,
585
+ isExporting,
586
+ progress,
587
+ exportAll,
588
+ exportWindows,
589
+ exportMacOS,
590
+ exportLinux,
591
+ downloadAll,
592
+ downloadSingle,
593
+ };
594
+ }
595
+ /* ══════════════════════════════════════════════════════════════════════════════
596
+ Component
597
+ ══════════════════════════════════════════════════════════════════════════════ */
598
+ /**
599
+ * Desktop icon exporter component with UI for configuring and downloading icons.
600
+ *
601
+ * @example
602
+ * ```tsx
603
+ * import { NiceDesktopIconExporter } from '@nice2dev/icons';
604
+ * import { NtdRocket } from '@nice2dev/icons/paths';
605
+ *
606
+ * function App() {
607
+ * return (
608
+ * <NiceDesktopIconExporter
609
+ * icon={NtdRocket}
610
+ * initialConfig={{ name: 'my-app', fillColor: '#ff6b35' }}
611
+ * />
612
+ * );
613
+ * }
614
+ * ```
615
+ */
616
+ const NiceDesktopIconExporter = ({ icon, initialConfig, onExport, className = '', }) => {
617
+ const { config, setConfig, icons, isExporting, progress, exportAll, downloadAll, downloadSingle, } = useDesktopIconExporter(icon, initialConfig);
618
+ const handleExport = async () => {
619
+ await exportAll();
620
+ onExport === null || onExport === void 0 ? void 0 : onExport(icons);
621
+ };
622
+ const IconComponent = icon;
623
+ return (jsxs("div", { className: `nice-desktop-icon-exporter ${className}`, children: [jsxs("div", { className: "nice-desktop-icon-exporter__preview", children: [jsx("h3", { children: "Preview" }), jsx("div", { className: "nice-desktop-icon-exporter__preview-grid", children: [16, 32, 48, 64, 128, 256].map((size) => (jsxs("div", { className: "nice-desktop-icon-exporter__preview-item", children: [jsx("div", { style: {
624
+ width: Math.min(size, 128),
625
+ height: Math.min(size, 128),
626
+ backgroundColor: config.backgroundColor || '#f0f0f0',
627
+ display: 'flex',
628
+ alignItems: 'center',
629
+ justifyContent: 'center',
630
+ borderRadius: 4,
631
+ }, children: jsx(IconComponent, { size: Math.min(size, 128) * (1 - config.padding * 2), fill: config.fillColor }) }), jsxs("span", { children: [size, "\u00D7", size] })] }, size))) })] }), jsxs("div", { className: "nice-desktop-icon-exporter__config", children: [jsx("h3", { children: "Configuration" }), jsxs("label", { children: ["Icon Name:", jsx("input", { type: "text", value: config.name, onChange: (e) => setConfig((c) => ({ ...c, name: e.target.value })) })] }), jsxs("label", { children: ["Fill Color:", jsx("input", { type: "color", value: config.fillColor, onChange: (e) => setConfig((c) => ({ ...c, fillColor: e.target.value })) })] }), jsxs("label", { children: ["Background:", jsx("input", { type: "color", value: config.backgroundColor || '#ffffff', onChange: (e) => setConfig((c) => ({ ...c, backgroundColor: e.target.value })) }), jsx("button", { onClick: () => setConfig((c) => ({ ...c, backgroundColor: undefined })), children: "Transparent" })] }), jsxs("label", { children: ["Padding: ", Math.round(config.padding * 100), "%", jsx("input", { type: "range", min: "0", max: "0.3", step: "0.01", value: config.padding, onChange: (e) => setConfig((c) => ({ ...c, padding: parseFloat(e.target.value) })) })] }), jsxs("div", { className: "nice-desktop-icon-exporter__platforms", children: [jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.includeWindows, onChange: (e) => setConfig((c) => ({ ...c, includeWindows: e.target.checked })) }), "Windows (.ico)"] }), jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.includeMacOS, onChange: (e) => setConfig((c) => ({ ...c, includeMacOS: e.target.checked })) }), "macOS (.iconset)"] }), jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.includeLinux, onChange: (e) => setConfig((c) => ({ ...c, includeLinux: e.target.checked })) }), "Linux (hicolor)"] }), jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.includeRetina, onChange: (e) => setConfig((c) => ({ ...c, includeRetina: e.target.checked })) }), "HiDPI/Retina (@2x, @3x)"] })] }), jsxs("div", { className: "nice-desktop-icon-exporter__sizes", children: [jsx("h4", { children: "Sizes:" }), DESKTOP_ICON_SIZES.map(({ size, label }) => (jsxs("label", { children: [jsx("input", { type: "checkbox", checked: config.sizes.includes(size), onChange: (e) => {
632
+ setConfig((c) => ({
633
+ ...c,
634
+ sizes: e.target.checked
635
+ ? [...c.sizes, size].sort((a, b) => a - b)
636
+ : c.sizes.filter((s) => s !== size),
637
+ }));
638
+ } }), label] }, size)))] })] }), jsxs("div", { className: "nice-desktop-icon-exporter__actions", children: [jsx("button", { onClick: handleExport, disabled: isExporting, children: isExporting ? `Exporting... ${Math.round(progress)}%` : 'Generate Icons' }), jsx("button", { onClick: downloadAll, disabled: isExporting || icons.length === 0, children: "Download All (ZIP)" })] }), icons.length > 0 && (jsxs("div", { className: "nice-desktop-icon-exporter__results", children: [jsxs("h3", { children: ["Generated Icons (", icons.length, ")"] }), jsx("div", { className: "nice-desktop-icon-exporter__results-grid", children: icons.map((exportedIcon) => (jsxs("div", { className: "nice-desktop-icon-exporter__result-item", children: [jsx("img", { src: exportedIcon.dataUrl, alt: exportedIcon.filename, style: { maxWidth: 64, maxHeight: 64 } }), jsx("span", { children: exportedIcon.filename }), jsx("button", { onClick: () => downloadSingle(exportedIcon), children: "Download" })] }, exportedIcon.filename))) })] }))] }));
639
+ };
640
+
641
+ export { DESKTOP_ICON_SIZES, LINUX_HICOLOR_DIRS, NiceDesktopIconExporter, RETINA_VARIANTS, useDesktopIconExporter };
642
+ //# sourceMappingURL=NiceDesktopIconExporter.js.map