@nice2dev/icons 1.0.5 → 1.0.6

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